Alerting: Update RBAC for alert rules to consider access to rule as access to group it belongs (#49033)

* update authz to exclude entire group if user does not have access to rule
* change rule update authz to not return changes because if user does not have access to any rule in group, they do not have access to the rule
* a new query that returns alerts in group by UID of alert that belongs to that group
* collect all affected groups during calculate changes
* update authorize to check access to groups
* update tests for calculateChanges to assert new fields
* add authorization tests
pull/50019/head
Yuriy Tseretyan 3 years ago committed by GitHub
parent 333195ce21
commit ad25e2a20c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      pkg/services/ngalert/api/api_prometheus.go
  2. 72
      pkg/services/ngalert/api/api_ruler.go
  3. 20
      pkg/services/ngalert/api/api_ruler_test.go
  4. 74
      pkg/services/ngalert/api/authorization.go
  5. 425
      pkg/services/ngalert/api/authorization_test.go
  6. 8
      pkg/services/ngalert/models/alert_rule.go
  7. 17
      pkg/services/ngalert/store/alert_rule.go
  8. 31
      pkg/services/ngalert/store/testing.go

@ -166,9 +166,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
for _, rule := range alertRuleQuery.Result { for _, rule := range alertRuleQuery.Result {
if !authorizeDatasourceAccessForRule(rule, hasAccess) {
continue
}
key := rule.GetGroupKey() key := rule.GetGroupKey()
rulesInGroup := groupedRules[key] rulesInGroup := groupedRules[key]
rulesInGroup = append(rulesInGroup, rule) rulesInGroup = append(rulesInGroup, rule)
@ -181,6 +178,9 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
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) 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 continue
} }
if !authorizeAccessToRuleGroup(rules, hasAccess) {
continue
}
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, srv.toRuleGroup(groupKey.RuleGroup, folder, rules, labelOptions)) ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, srv.toRuleGroup(groupKey.RuleGroup, folder, rules, labelOptions))
} }
return response.JSON(http.StatusOK, ruleResponse) return response.JSON(http.StatusOK, ruleResponse)

@ -238,7 +238,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Resp
groupRules := make([]*ngmodels.AlertRule, 0, len(q.Result)) groupRules := make([]*ngmodels.AlertRule, 0, len(q.Result))
for _, r := range q.Result { for _, r := range q.Result {
if !authorizeDatasourceAccessForRule(r, hasAccess) { if !authorizeDatasourceAccessForRule(r, hasAccess) {
continue return ErrResp(http.StatusUnauthorized, fmt.Errorf("%w to access the group because it does not have access to one or many data sources one or many rules in the group use", ErrAuthorization), "")
} }
groupRules = append(groupRules, r) groupRules = append(groupRules, r)
} }
@ -297,9 +297,6 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
configs := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) configs := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
for _, r := range q.Result { for _, r := range q.Result {
if !authorizeDatasourceAccessForRule(r, hasAccess) {
continue
}
groupKey := r.GetGroupKey() groupKey := r.GetGroupKey()
group := configs[groupKey] group := configs[groupKey]
group = append(group, r) group = append(group, r)
@ -312,6 +309,9 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", groupKey.NamespaceUID) srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", groupKey.NamespaceUID)
continue continue
} }
if !authorizeAccessToRuleGroup(rules, hasAccess) {
continue
}
namespace := folder.Title namespace := folder.Title
result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, folder.Id, provenanceRecords)) result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, folder.Id, provenanceRecords))
} }
@ -358,21 +358,14 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return nil return nil
} }
authorizedChanges := groupChanges // if RBAC is disabled the permission are limited to folder access that is done upstream // if RBAC is disabled the permission are limited to folder access that is done upstream
if !srv.ac.IsDisabled() { if !srv.ac.IsDisabled() {
authorizedChanges, err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool { err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator) return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator)
}) })
if err != nil { if err != nil {
return err return err
} }
if authorizedChanges.isEmpty() {
logger.Info("no authorized changes detected in the request. Do nothing", "not_authorized_add", len(groupChanges.New), "not_authorized_update", len(groupChanges.Update), "not_authorized_delete", len(groupChanges.Delete))
return nil
}
if len(groupChanges.Delete) > len(authorizedChanges.Delete) {
logger.Info("user is not authorized to delete one or many rules in the group. those rules will be skipped", "expected", len(groupChanges.Delete), "authorized", len(authorizedChanges.Delete))
}
} }
provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.OrgId, (&ngmodels.AlertRule{}).ResourceType()) provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.OrgId, (&ngmodels.AlertRule{}).ResourceType())
@ -382,13 +375,13 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
// New rules don't need to be checked for provenance, just copy the whole slice. // New rules don't need to be checked for provenance, just copy the whole slice.
finalChanges = &changes{} finalChanges = &changes{}
finalChanges.New = authorizedChanges.New finalChanges.New = groupChanges.New
for _, rule := range authorizedChanges.Update { for _, rule := range groupChanges.Update {
if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists { if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Update = append(finalChanges.Update, rule) finalChanges.Update = append(finalChanges.Update, rule)
} }
} }
for _, rule := range authorizedChanges.Delete { for _, rule := range groupChanges.Delete {
if provenance, exists := provenances[rule.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists { if provenance, exists := provenances[rule.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Delete = append(finalChanges.Delete, rule) finalChanges.Delete = append(finalChanges.Delete, rule)
} }
@ -396,22 +389,22 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
if finalChanges.isEmpty() { if finalChanges.isEmpty() {
logger.Info("no changes detected that have 'none' provenance in the request. Do nothing", logger.Info("no changes detected that have 'none' provenance in the request. Do nothing",
"provenance_invalid_add", len(authorizedChanges.New), "provenance_invalid_add", len(groupChanges.New),
"provenance_invalid_update", len(authorizedChanges.Update), "provenance_invalid_update", len(groupChanges.Update),
"provenance_invalid_delete", len(authorizedChanges.Delete)) "provenance_invalid_delete", len(groupChanges.Delete))
return nil return nil
} }
if len(authorizedChanges.Delete) > len(finalChanges.Delete) { if len(groupChanges.Delete) > len(finalChanges.Delete) {
logger.Info("provenance is not 'none' for one or many rules in the group that should be deleted. those rules will be skipped", logger.Info("provenance is not 'none' for one or many rules in the group that should be deleted. those rules will be skipped",
"expected", len(authorizedChanges.Delete), "expected", len(groupChanges.Delete),
"allowed", len(authorizedChanges.Delete)) "allowed", len(groupChanges.Delete))
} }
if len(authorizedChanges.Update) > len(finalChanges.Update) { if len(groupChanges.Update) > len(finalChanges.Update) {
logger.Info("provenance is not 'none' for one or many rules in the group that should be updated. those rules will be skipped", logger.Info("provenance is not 'none' for one or many rules in the group that should be updated. those rules will be skipped",
"expected", len(authorizedChanges.Update), "expected", len(groupChanges.Update),
"allowed", len(authorizedChanges.Update)) "allowed", len(groupChanges.Update))
} }
logger.Debug("updating database with the authorized changes", "add", len(finalChanges.New), "update", len(finalChanges.New), "delete", len(finalChanges.Delete)) logger.Debug("updating database with the authorized changes", "add", len(finalChanges.New), "update", len(finalChanges.New), "delete", len(finalChanges.Delete))
@ -565,6 +558,7 @@ type ruleUpdate struct {
type changes struct { type changes struct {
GroupKey ngmodels.AlertRuleGroupKey GroupKey ngmodels.AlertRuleGroupKey
AffectedGroups map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule
New []*ngmodels.AlertRule New []*ngmodels.AlertRule
Update []ruleUpdate Update []ruleUpdate
Delete []*ngmodels.AlertRule Delete []*ngmodels.AlertRule
@ -577,6 +571,7 @@ 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). // 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. // 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, groupKey ngmodels.AlertRuleGroupKey, submittedRules []*ngmodels.AlertRule) (*changes, error) { func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey ngmodels.AlertRuleGroupKey, submittedRules []*ngmodels.AlertRule) (*changes, error) {
affectedGroups := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
q := &ngmodels.ListAlertRulesQuery{ q := &ngmodels.ListAlertRulesQuery{
OrgID: groupKey.OrgID, OrgID: groupKey.OrgID,
NamespaceUIDs: []string{groupKey.NamespaceUID}, NamespaceUIDs: []string{groupKey.NamespaceUID},
@ -586,6 +581,9 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", groupKey, err) return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", groupKey, err)
} }
existingGroupRules := q.Result existingGroupRules := q.Result
if len(existingGroupRules) > 0 {
affectedGroups[groupKey] = existingGroupRules
}
existingGroupRulesUIDs := make(map[string]*ngmodels.AlertRule, len(existingGroupRules)) existingGroupRulesUIDs := make(map[string]*ngmodels.AlertRule, len(existingGroupRules))
for _, r := range existingGroupRules { for _, r := range existingGroupRules {
@ -594,25 +592,30 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
var toAdd, toDelete []*ngmodels.AlertRule var toAdd, toDelete []*ngmodels.AlertRule
var toUpdate []ruleUpdate var toUpdate []ruleUpdate
loadedRulesByUID := map[string]*ngmodels.AlertRule{} // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group
for _, r := range submittedRules { for _, r := range submittedRules {
var existing *ngmodels.AlertRule = nil var existing *ngmodels.AlertRule = nil
if r.UID != "" { if r.UID != "" {
if existingGroupRule, ok := existingGroupRulesUIDs[r.UID]; ok { if existingGroupRule, ok := existingGroupRulesUIDs[r.UID]; ok {
existing = existingGroupRule existing = existingGroupRule
// remove the rule from existingGroupRulesUIDs // remove the rule from existingGroupRulesUIDs
delete(existingGroupRulesUIDs, r.UID) delete(existingGroupRulesUIDs, r.UID)
} else { } else if existing, ok = loadedRulesByUID[r.UID]; !ok { // check the "cache" and if there is no hit, query the database
// Rule can be from other group or namespace // Rule can be from other group or namespace
q := &ngmodels.GetAlertRuleByUIDQuery{OrgID: groupKey.OrgID, UID: r.UID} q := &ngmodels.GetAlertRulesGroupByRuleUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
if err := ruleStore.GetAlertRuleByUID(ctx, q); err != nil || q.Result == nil { if err := ruleStore.GetAlertRulesGroupByRuleUID(ctx, q); err != nil {
// if rule has UID then it is considered an update. Therefore, fail if there is no rule to update return nil, fmt.Errorf("failed to query database for a group of alert rules: %w", err)
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) || q.Result == nil && err == nil {
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, ngmodels.ErrAlertRuleNotFound)
} }
return nil, fmt.Errorf("failed to query database for an alert rule with UID %s: %w", r.UID, err) for _, rule := range q.Result {
if rule.UID == r.UID {
existing = rule
}
loadedRulesByUID[rule.UID] = rule
}
if existing == nil {
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, ngmodels.ErrAlertRuleNotFound)
} }
existing = q.Result affectedGroups[existing.GetGroupKey()] = q.Result
} }
} }
@ -642,6 +645,7 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
return &changes{ return &changes{
GroupKey: groupKey, GroupKey: groupKey,
AffectedGroups: affectedGroups,
New: toAdd, New: toAdd,
Delete: toDelete, Delete: toDelete,
Update: toUpdate, Update: toUpdate,

@ -64,6 +64,7 @@ func TestCalculateChanges(t *testing.T) {
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, make([]*models.AlertRule, 0)) changes, err := calculateChanges(context.Background(), fakeStore, groupKey, make([]*models.AlertRule, 0))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, groupKey, changes.GroupKey)
require.Empty(t, changes.New) require.Empty(t, changes.New)
require.Empty(t, changes.Update) require.Empty(t, changes.Update)
require.Len(t, changes.Delete, len(inDatabaseMap)) require.Len(t, changes.Delete, len(inDatabaseMap))
@ -72,6 +73,8 @@ func TestCalculateChanges(t *testing.T) {
db := inDatabaseMap[toDelete.UID] db := inDatabaseMap[toDelete.UID]
require.Equal(t, db, toDelete) require.Equal(t, db, toDelete)
} }
require.Contains(t, changes.AffectedGroups, groupKey)
require.Equal(t, inDatabase, changes.AffectedGroups[groupKey])
}) })
t.Run("should detect alerts that needs to be updated", func(t *testing.T) { t.Run("should detect alerts that needs to be updated", func(t *testing.T) {
@ -85,6 +88,7 @@ func TestCalculateChanges(t *testing.T) {
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted) changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, groupKey, changes.GroupKey)
require.Len(t, changes.Update, len(inDatabase)) require.Len(t, changes.Update, len(inDatabase))
for _, upsert := range changes.Update { for _, upsert := range changes.Update {
require.NotNil(t, upsert.Existing) require.NotNil(t, upsert.Existing)
@ -95,6 +99,9 @@ func TestCalculateChanges(t *testing.T) {
} }
require.Empty(t, changes.Delete) require.Empty(t, changes.Delete)
require.Empty(t, changes.New) require.Empty(t, changes.New)
require.Contains(t, changes.AffectedGroups, groupKey)
require.Equal(t, inDatabase, changes.AffectedGroups[groupKey])
}) })
t.Run("should include only if there are changes ignoring specific fields", func(t *testing.T) { t.Run("should include only if there are changes ignoring specific fields", func(t *testing.T) {
@ -187,7 +194,8 @@ func TestCalculateChanges(t *testing.T) {
}) })
t.Run("should be able to find alerts by UID in other group/namespace", func(t *testing.T) { t.Run("should be able to find alerts by UID in other group/namespace", func(t *testing.T) {
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(10)+10, models.AlertRuleGen(withOrgID(orgId))) sourceGroupKey := models.GenerateGroupKey(orgId)
inDatabaseMap, inDatabase := models.GenerateUniqueAlertRules(rand.Intn(10)+10, models.AlertRuleGen(withGroupKey(sourceGroupKey)))
fakeStore := store.NewFakeRuleStore(t) fakeStore := store.NewFakeRuleStore(t)
fakeStore.PutRule(context.Background(), inDatabase...) fakeStore.PutRule(context.Background(), inDatabase...)
@ -206,6 +214,7 @@ func TestCalculateChanges(t *testing.T) {
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted) changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, groupKey, changes.GroupKey)
require.Empty(t, changes.Delete) require.Empty(t, changes.Delete)
require.Empty(t, changes.New) require.Empty(t, changes.New)
require.Len(t, changes.Update, len(submitted)) require.Len(t, changes.Update, len(submitted))
@ -216,6 +225,11 @@ func TestCalculateChanges(t *testing.T) {
require.Equal(t, submittedMap[update.Existing.UID], update.New) require.Equal(t, submittedMap[update.Existing.UID], update.New)
require.NotEmpty(t, update.Diff) require.NotEmpty(t, update.Diff)
} }
require.Contains(t, changes.AffectedGroups, sourceGroupKey)
require.NotContains(t, changes.AffectedGroups, groupKey) // because there is no such group in database yet
require.Len(t, changes.AffectedGroups[sourceGroupKey], len(inDatabase))
}) })
t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) { t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) {
@ -251,7 +265,7 @@ func TestCalculateChanges(t *testing.T) {
expectedErr := errors.New("TEST ERROR") expectedErr := errors.New("TEST ERROR")
fakeStore.Hook = func(cmd interface{}) error { fakeStore.Hook = func(cmd interface{}) error {
switch cmd.(type) { switch cmd.(type) {
case models.GetAlertRuleByUIDQuery: case models.GetAlertRulesGroupByRuleUIDQuery:
return expectedErr return expectedErr
} }
return nil return nil
@ -261,7 +275,7 @@ func TestCalculateChanges(t *testing.T) {
submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)() submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
_, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{submitted}) _, err := calculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRule{submitted})
require.Error(t, err, expectedErr) require.ErrorIs(t, err, expectedErr)
}) })
} }

@ -219,34 +219,43 @@ func authorizeDatasourceAccessForRule(rule *ngmodels.AlertRule, evaluator func(e
return true return true
} }
// authorizeAccessToRuleGroup checks all rules against authorizeDatasourceAccessForRule and exits on the first negative result
func authorizeAccessToRuleGroup(rules []*ngmodels.AlertRule, evaluator func(evaluator ac.Evaluator) bool) bool {
for _, rule := range rules {
if !authorizeDatasourceAccessForRule(rule, evaluator) {
return false
}
}
return true
}
// authorizeRuleChanges analyzes changes in the rule group, and checks whether the changes are authorized. // authorizeRuleChanges analyzes changes in the rule group, and checks whether the changes are authorized.
// 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. // 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. // 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 // Return changes that the user is authorized to perform or ErrAuthorization
func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) (*changes, error) { func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) error {
var result = &changes{
GroupKey: change.GroupKey,
New: change.New,
Update: change.Update,
Delete: change.Delete,
}
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID) namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
if len(change.Delete) > 0 {
var allowedToDelete []*ngmodels.AlertRule rules, ok := change.AffectedGroups[change.GroupKey]
for _, rule := range change.Delete { if ok { // not ok can be when user creates a new rule group or moves existing alerts to a new group
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator) if !authorizeAccessToRuleGroup(rules, evaluator) { // if user is not authorized to do operation in the group that is being changed
if dsAllowed { return fmt.Errorf("%w to change group %s because it does not have access to one or many rules in this group", ErrAuthorization, change.GroupKey.RuleGroup)
allowedToDelete = append(allowedToDelete, rule)
} }
} else if len(change.Delete) > 0 {
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
return fmt.Errorf("failed to authorize changes in rule group %s. Detected %d deletes but group was not provided", change.GroupKey.RuleGroup, len(change.Delete))
} }
if len(allowedToDelete) > 0 {
if len(change.Delete) > 0 {
allowed := evaluator(ac.EvalPermission(ac.ActionAlertingRuleDelete, namespaceScope)) allowed := evaluator(ac.EvalPermission(ac.ActionAlertingRuleDelete, namespaceScope))
if !allowed { if !allowed {
return nil, fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID) return fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
for _, rule := range change.Delete {
if !authorizeDatasourceAccessForRule(rule, evaluator) {
return fmt.Errorf("%w to delete an alert rule '%s' because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.UID)
} }
} }
result.Delete = allowedToDelete
} }
var addAuthorized, updateAuthorized bool var addAuthorized, updateAuthorized bool
@ -254,12 +263,12 @@ func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator
if len(change.New) > 0 { if len(change.New) > 0 {
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope)) addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
if !addAuthorized { if !addAuthorized {
return nil, fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID) return fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
} }
for _, rule := range change.New { for _, rule := range change.New {
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator) dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
if !dsAllowed { if !dsAllowed {
return nil, fmt.Errorf("%w to create a new alert rule '%s' because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Title) return fmt.Errorf("%w to create a new alert rule '%s' because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Title)
} }
} }
} }
@ -267,31 +276,40 @@ func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator
for _, rule := range change.Update { for _, rule := range change.Update {
dsAllowed := authorizeDatasourceAccessForRule(rule.New, evaluator) dsAllowed := authorizeDatasourceAccessForRule(rule.New, evaluator)
if !dsAllowed { if !dsAllowed {
return nil, fmt.Errorf("%w to update alert rule '%s' (UID: %s) because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Existing.Title, rule.Existing.UID) return fmt.Errorf("%w to update alert rule '%s' (UID: %s) because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Existing.Title, rule.Existing.UID)
} }
// Check if the rule is moved from one folder to the current. If yes, then the user must have the authorization to delete rules from the source folder and add rules to the target folder. // Check if the rule is moved from one folder to the current. If yes, then the user must have the authorization to delete rules from the source folder and add rules to the target folder.
if rule.Existing.NamespaceUID != rule.New.NamespaceUID { if rule.Existing.NamespaceUID != rule.New.NamespaceUID {
allowed := evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID)))) allowed := evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID))))
if !allowed { if !allowed {
return nil, fmt.Errorf("%w to delete alert rules from folder UID %s", ErrAuthorization, rule.Existing.NamespaceUID) return fmt.Errorf("%w to delete alert rules from folder UID %s", ErrAuthorization, rule.Existing.NamespaceUID)
} }
if !addAuthorized { if !addAuthorized {
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope)) addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
if !addAuthorized { if !addAuthorized {
return nil, fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID) return fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID)
} }
} }
continue } else 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.EvalPermission(ac.ActionAlertingRuleUpdate, namespaceScope))
if !updateAuthorized {
return fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
} }
if !updateAuthorized { // if it is false then the authorization was not checked. If it is true then the user is authorized to update rules if rule.Existing.NamespaceUID != rule.New.NamespaceUID || rule.Existing.RuleGroup != rule.New.RuleGroup {
updateAuthorized = evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleUpdate, namespaceScope))) key := rule.Existing.GetGroupKey()
if !updateAuthorized { rules, ok = change.AffectedGroups[key]
return nil, fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID) if !ok {
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
return fmt.Errorf("failed to authorize moving an alert rule %s between groups because unable to check access to group %s from which the rule is moved", rule.Existing.UID, rule.Existing.RuleGroup)
}
if !authorizeAccessToRuleGroup(rules, evaluator) {
return fmt.Errorf("%w to move rule %s between two different groups because user does not have access to the source group %s", ErrAuthorization, rule.Existing.UID, rule.Existing.RuleGroup)
} }
} }
} }
return result, nil return nil
} }

@ -1,6 +1,7 @@
package api package api
import ( import (
"math"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
@ -16,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
) )
func TestAuthorize(t *testing.T) { func TestAuthorize(t *testing.T) {
@ -68,6 +70,67 @@ func TestAuthorize(t *testing.T) {
}) })
} }
func createAllCombinationsOfPermissions(permissions map[string][]string) []map[string][]string {
type actionscope struct {
action string
scope string
}
var flattenPermissions []actionscope
for action, scopes := range permissions {
for _, scope := range scopes {
flattenPermissions = append(flattenPermissions, actionscope{
action,
scope,
})
}
}
l := len(flattenPermissions)
// this is all possible combinations of the permissions
var permissionCombinations []map[string][]string
for bit := uint(0); bit < uint(math.Pow(2, float64(l))); bit++ {
var tuple []actionscope
for idx := 0; idx < l; idx++ {
if (bit>>idx)&1 == 1 {
tuple = append(tuple, flattenPermissions[idx])
}
}
combination := make(map[string][]string)
for _, perm := range tuple {
combination[perm.action] = append(combination[perm.action], perm.scope)
}
permissionCombinations = append(permissionCombinations, combination)
}
return permissionCombinations
}
func getDatasourceScopesForRules(rules []*models.AlertRule) []string {
scopesMap := map[string]struct{}{}
var result []string
for _, rule := range rules {
for _, query := range rule.Data {
scope := datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)
if _, ok := scopesMap[scope]; ok {
continue
}
result = append(result, scope)
scopesMap[scope] = struct{}{}
}
}
return result
}
func mapUpdates(updates []ruleUpdate, mapFunc func(ruleUpdate) *models.AlertRule) []*models.AlertRule {
result := make([]*models.AlertRule, 0, len(updates))
for _, update := range updates {
result = append(result, mapFunc(update))
}
return result
}
func TestAuthorizeRuleChanges(t *testing.T) { func TestAuthorizeRuleChanges(t *testing.T) {
groupKey := models.GenerateGroupKey(rand.Int63()) groupKey := models.GenerateGroupKey(rand.Int63())
namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID) namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
@ -103,251 +166,195 @@ func TestAuthorizeRuleChanges(t *testing.T) {
}, },
}, },
{ {
name: "if there are rules to update within the same namespace it should check update action", name: "if there are rules to delete it should check delete action and query for datasource",
changes: func() *changes { changes: func() *changes {
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey))) rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
updates := make([]ruleUpdate, 0, len(rules)) rules2 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
for _, rule := range rules {
updates = append(updates, ruleUpdate{
Existing: rule,
New: models.CopyRule(rule),
Diff: nil,
})
}
return &changes{ return &changes{
GroupKey: groupKey, GroupKey: groupKey,
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
groupKey: append(rules, rules2...),
},
New: nil, New: nil,
Update: updates, Update: nil,
Delete: nil, Delete: rules2,
} }
}, },
permissions: func(c *changes) map[string][]string { permissions: func(c *changes) map[string][]string {
var scopes []string
for _, update := range c.Update {
for _, query := range update.New.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return map[string][]string{ return map[string][]string{
ac.ActionAlertingRuleUpdate: { ac.ActionAlertingRuleDelete: {
namespaceIdScope, namespaceIdScope,
}, },
datasources.ActionQuery: scopes, datasources.ActionQuery: getDatasourceScopesForRules(c.AffectedGroups[c.GroupKey]),
} }
}, },
}, },
{ {
name: "if there are rules that are moved between namespaces it should check update action", name: "if there are rules to update within the same namespace it should check update action and access to datasource",
changes: func() *changes { changes: func() *changes {
rules1 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey))) rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
updates := make([]ruleUpdate, 0, len(rules)) updates := make([]ruleUpdate, 0, len(rules))
for _, rule := range rules { for _, rule := range rules {
cp := models.CopyRule(rule) cp := models.CopyRule(rule)
cp.NamespaceUID = rule.NamespaceUID + "other" cp.Data = []models.AlertQuery{models.GenerateAlertQuery()}
updates = append(updates, ruleUpdate{ updates = append(updates, ruleUpdate{
Existing: cp, Existing: rule,
New: rule, New: cp,
Diff: nil, Diff: nil,
}) })
} }
return &changes{ return &changes{
GroupKey: groupKey, GroupKey: groupKey,
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
groupKey: append(rules, rules1...),
},
New: nil, New: nil,
Update: updates, Update: updates,
Delete: nil, Delete: nil,
} }
}, },
permissions: func(c *changes) map[string][]string { permissions: func(c *changes) map[string][]string {
var scopes []string scopes := getDatasourceScopesForRules(append(c.AffectedGroups[c.GroupKey], mapUpdates(c.Update, func(update ruleUpdate) *models.AlertRule {
for _, update := range c.Update { return update.New
for _, query := range update.New.Data { })...))
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
return map[string][]string{ return map[string][]string{
ac.ActionAlertingRuleDelete: { ac.ActionAlertingRuleUpdate: {
dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID + "other"),
},
ac.ActionAlertingRuleCreate: {
namespaceIdScope, namespaceIdScope,
}, },
datasources.ActionQuery: scopes, datasources.ActionQuery: scopes,
} }
}, },
}, },
} {
name: "if there are rules that are moved between namespaces it should check delete+add action and access to group where rules come from",
for _, testCase := range testCases { changes: func() *changes {
t.Run(testCase.name, func(t *testing.T) { rules1 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
executed := false rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
groupChanges := testCase.changes()
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(make(map[string][]string))
require.False(t, response)
executed = true
return false
})
require.Nil(t, result)
require.Error(t, err)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
permissions := testCase.permissions(groupChanges)
executed = false
result, err = authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(permissions)
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", testCase.permissions, evaluator.GoString())
executed = true
return true
})
require.NoError(t, err)
require.Equal(t, groupChanges, result)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
})
}
}
func TestAuthorizeRuleDelete(t *testing.T) { targetGroupKey := models.GenerateGroupKey(groupKey.OrgID)
groupKey := models.GenerateGroupKey(rand.Int63())
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
getScopes := func(rules []*models.AlertRule) []string { updates := make([]ruleUpdate, 0, len(rules))
var scopes []string
for _, rule := range rules { for _, rule := range rules {
for _, query := range rule.Data { cp := models.CopyRule(rule)
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)) withGroupKey(targetGroupKey)(cp)
} cp.Data = []models.AlertQuery{
models.GenerateAlertQuery(),
} }
return scopes
updates = append(updates, ruleUpdate{
Existing: rule,
New: cp,
})
} }
testCases := []struct {
name string
changes func() *changes
permissions func(c *changes) map[string][]string
assert func(t *testing.T, orig, authz *changes, err error)
}{
{
name: "should validate check access to data source and folder",
changes: func() *changes {
return &changes{ return &changes{
GroupKey: groupKey, GroupKey: targetGroupKey,
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
groupKey: append(rules, rules1...),
},
New: nil, New: nil,
Update: nil, Update: updates,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))), Delete: nil,
} }
}, },
permissions: func(c *changes) map[string][]string { permissions: func(c *changes) map[string][]string {
dsScopes := getDatasourceScopesForRules(
append(append(append(c.AffectedGroups[c.GroupKey],
mapUpdates(c.Update, func(update ruleUpdate) *models.AlertRule {
return update.New
})...,
), mapUpdates(c.Update, func(update ruleUpdate) *models.AlertRule {
return update.Existing
})...), c.AffectedGroups[groupKey]...),
)
var deleteScopes []string
for key := range c.AffectedGroups {
deleteScopes = append(deleteScopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(key.NamespaceUID))
}
return map[string][]string{ return map[string][]string{
ac.ActionAlertingRuleDelete: { ac.ActionAlertingRuleDelete: deleteScopes,
namespaceScope, ac.ActionAlertingRuleCreate: {
dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID),
}, },
datasources.ActionQuery: getScopes(c.Delete), datasources.ActionQuery: dsScopes,
} }
}, },
assert: func(t *testing.T, orig, authz *changes, err error) {
require.NoError(t, err)
require.Equal(t, orig, authz)
},
}, },
{ {
name: "should remove rules user does not have access to data source", name: "if there are rules that are moved between groups in the same namespace it should check update action and access to all groups (source+target)",
changes: func() *changes { changes: func() *changes {
return &changes{ targetGroupKey := models.AlertRuleGroupKey{
GroupKey: groupKey, OrgID: groupKey.OrgID,
New: nil, NamespaceUID: groupKey.NamespaceUID,
Update: nil, RuleGroup: util.GenerateShortUID(),
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
} }
}, sourceGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
permissions: func(c *changes) map[string][]string { targetGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(targetGroupKey)))
return map[string][]string{
ac.ActionAlertingRuleDelete: { updates := make([]ruleUpdate, 0, len(sourceGroup))
namespaceScope, toCopy := len(sourceGroup)
}, if toCopy > 1 {
datasources.ActionQuery: { toCopy = rand.Intn(toCopy-1) + 1
getScopes(c.Delete[:1])[0],
},
} }
}, for i := 0; i < toCopy; i++ {
assert: func(t *testing.T, orig, authz *changes, err error) { rule := sourceGroup[0]
require.NoError(t, err) cp := models.CopyRule(rule)
require.Greater(t, len(orig.Delete), len(authz.Delete)) withGroupKey(targetGroupKey)(cp)
}, cp.Data = []models.AlertQuery{
}, models.GenerateAlertQuery(),
{ }
name: "should not fail if no changes other than unauthorized",
changes: func() *changes { updates = append(updates, ruleUpdate{
Existing: rule,
New: cp,
})
}
return &changes{ return &changes{
GroupKey: groupKey, GroupKey: targetGroupKey,
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
groupKey: sourceGroup,
targetGroupKey: targetGroup,
},
New: nil, New: nil,
Update: nil, Update: updates,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))), Delete: nil,
} }
}, },
permissions: func(c *changes) map[string][]string { permissions: func(c *changes) map[string][]string {
return map[string][]string{ scopes := make(map[string]struct{})
ac.ActionAlertingRuleDelete: { for _, update := range c.Update {
namespaceScope, for _, query := range update.New.Data {
}, scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
} }
}, for _, query := range update.Existing.Data {
assert: func(t *testing.T, orig, authz *changes, err error) { scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
require.NoError(t, err)
require.False(t, orig.isEmpty())
require.True(t, authz.isEmpty())
},
},
{
name: "should not fail if there are changes and no rules can be deleted",
changes: func() *changes {
return &changes{
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 {
return map[string][]string{
ac.ActionAlertingRuleDelete: {
namespaceScope,
},
ac.ActionAlertingRuleCreate: {
namespaceScope,
},
datasources.ActionQuery: getScopes(c.New),
} }
}, for _, rules := range c.AffectedGroups {
assert: func(t *testing.T, _, c *changes, err error) { for _, rule := range rules {
require.NoError(t, err) for _, query := range rule.Data {
require.Empty(t, c.Delete) scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
},
},
{
name: "should fail if no access to folder",
changes: func() *changes {
return &changes{
GroupKey: groupKey,
New: nil,
Update: nil,
Delete: models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))),
} }
},
permissions: func(c *changes) map[string][]string {
return map[string][]string{
datasources.ActionQuery: getScopes(c.Delete),
} }
}
dsScopes := make([]string, 0, len(scopes))
for key := range scopes {
dsScopes = append(dsScopes, key)
}
return map[string][]string{
ac.ActionAlertingRuleUpdate: {
dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID),
}, },
assert: func(t *testing.T, _, c *changes, err error) { datasources.ActionQuery: dsScopes,
require.ErrorIs(t, err, ErrAuthorization) }
require.Nil(t, c)
}, },
}, },
} }
@ -356,12 +363,33 @@ func TestAuthorizeRuleDelete(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
groupChanges := testCase.changes() groupChanges := testCase.changes()
permissions := testCase.permissions(groupChanges) permissions := testCase.permissions(groupChanges)
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(permissions) t.Run("should fail with insufficient permissions", func(t *testing.T) {
permissionCombinations := createAllCombinationsOfPermissions(permissions)
permissionCombinations = permissionCombinations[0 : len(permissionCombinations)-1] // exclude all permissions
for _, missing := range permissionCombinations {
executed := false
err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(missing)
executed = true
return response return response
}) })
require.Errorf(t, err, "expected error because less permissions than expected were provided. Provided: %v; Expected: %v", missing, permissions)
require.ErrorIs(t, err, ErrAuthorization)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
}
})
executed := false
testCase.assert(t, groupChanges, result, err) err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(permissions)
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", permissions, evaluator.GoString())
executed = true
return true
})
require.NoError(t, err)
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
}) })
} }
} }
@ -420,3 +448,48 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
require.Equal(t, 1, executed) require.Equal(t, 1, executed)
}) })
} }
func Test_authorizeAccessToRuleGroup(t *testing.T) {
t.Run("should return true if user has access to all datasources of all rules in group", func(t *testing.T) {
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen())
var scopes []string
for _, rule := range rules {
for _, query := range rule.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
permissions := map[string][]string{
datasources.ActionQuery: scopes,
}
result := authorizeAccessToRuleGroup(rules, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(permissions)
require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", permissions, evaluator.GoString())
return true
})
require.True(t, result)
})
t.Run("should return false if user does not have access to at least one rule in group", func(t *testing.T) {
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen())
var scopes []string
for _, rule := range rules {
for _, query := range rule.Data {
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
}
}
permissions := map[string][]string{
datasources.ActionQuery: scopes,
}
rule := models.AlertRuleGen()()
rules = append(rules, rule)
result := authorizeAccessToRuleGroup(rules, func(evaluator ac.Evaluator) bool {
response := evaluator.Evaluate(permissions)
return response
})
require.False(t, result)
})
}

@ -266,6 +266,14 @@ type GetAlertRuleByUIDQuery struct {
Result *AlertRule Result *AlertRule
} }
// GetAlertRulesGroupByRuleUIDQuery is the query for retrieving a group of alerts by UID of a rule that belongs to that group
type GetAlertRulesGroupByRuleUIDQuery struct {
UID string
OrgID int64
Result []*AlertRule
}
// ListAlertRulesQuery is the query for listing alert rules // ListAlertRulesQuery is the query for listing alert rules
type ListAlertRulesQuery struct { type ListAlertRulesQuery struct {
OrgID int64 OrgID int64

@ -37,6 +37,7 @@ type RuleStore interface {
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error
GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) error
GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error
ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
// GetRuleGroups returns the unique rule groups across all organizations. // GetRuleGroups returns the unique rule groups across all organizations.
@ -109,6 +110,22 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler
}) })
} }
// GetAlertRulesGroupByRuleUID is a handler for retrieving a group of alert rules from that database by UID and organisation ID of one of rules that belong to that group.
func (st DBstore) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var result []*ngmodels.AlertRule
err := sess.Table("alert_rule").Alias("A").Join(
"INNER",
"alert_rule AS B", "A.org_id = B.org_id AND A.namespace_uid = B.namespace_uid AND A.rule_group = B.rule_group AND B.uid = ?", query.UID,
).Where("A.org_id = ?", query.OrgID).Select("A.*").Find(&result)
if err != nil {
return err
}
query.Result = result
return nil
})
}
// InsertAlertRules is a handler for creating/updating alert rules. // InsertAlertRules is a handler for creating/updating alert rules.
func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) error { func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {

@ -152,6 +152,37 @@ func (f *FakeRuleStore) GetAlertRuleByUID(_ context.Context, q *models.GetAlertR
return nil return nil
} }
func (f *FakeRuleStore) GetAlertRulesGroupByRuleUID(_ context.Context, q *models.GetAlertRulesGroupByRuleUIDQuery) error {
f.mtx.Lock()
defer f.mtx.Unlock()
f.RecordedOps = append(f.RecordedOps, *q)
if err := f.Hook(*q); err != nil {
return err
}
rules, ok := f.Rules[q.OrgID]
if !ok {
return nil
}
var selected *models.AlertRule
for _, rule := range rules {
if rule.UID == q.UID {
selected = rule
break
}
}
if selected == nil {
return nil
}
for _, rule := range rules {
if rule.GetGroupKey() == selected.GetGroupKey() {
q.Result = append(q.Result, rule)
}
}
return nil
}
// For now, we're not implementing namespace filtering. // For now, we're not implementing namespace filtering.
func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models.GetAlertRulesForSchedulingQuery) error { func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models.GetAlertRulesForSchedulingQuery) error {
f.mtx.Lock() f.mtx.Lock()

Loading…
Cancel
Save