mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
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
This commit is contained in:
parent
333195ce21
commit
ad25e2a20c
@ -166,9 +166,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
|
||||
|
||||
groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
|
||||
for _, rule := range alertRuleQuery.Result {
|
||||
if !authorizeDatasourceAccessForRule(rule, hasAccess) {
|
||||
continue
|
||||
}
|
||||
key := rule.GetGroupKey()
|
||||
rulesInGroup := groupedRules[key]
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if !authorizeAccessToRuleGroup(rules, hasAccess) {
|
||||
continue
|
||||
}
|
||||
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, srv.toRuleGroup(groupKey.RuleGroup, folder, rules, labelOptions))
|
||||
}
|
||||
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))
|
||||
for _, r := range q.Result {
|
||||
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)
|
||||
}
|
||||
@ -297,9 +297,6 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
|
||||
|
||||
configs := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
|
||||
for _, r := range q.Result {
|
||||
if !authorizeDatasourceAccessForRule(r, hasAccess) {
|
||||
continue
|
||||
}
|
||||
groupKey := r.GetGroupKey()
|
||||
group := configs[groupKey]
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if !authorizeAccessToRuleGroup(rules, hasAccess) {
|
||||
continue
|
||||
}
|
||||
namespace := folder.Title
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
authorizedChanges, err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
|
||||
err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
|
||||
return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator)
|
||||
})
|
||||
if err != nil {
|
||||
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())
|
||||
@ -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.
|
||||
finalChanges = &changes{}
|
||||
finalChanges.New = authorizedChanges.New
|
||||
for _, rule := range authorizedChanges.Update {
|
||||
finalChanges.New = groupChanges.New
|
||||
for _, rule := range groupChanges.Update {
|
||||
if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
|
||||
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 {
|
||||
finalChanges.Delete = append(finalChanges.Delete, rule)
|
||||
}
|
||||
@ -396,22 +389,22 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
|
||||
|
||||
if finalChanges.isEmpty() {
|
||||
logger.Info("no changes detected that have 'none' provenance in the request. Do nothing",
|
||||
"provenance_invalid_add", len(authorizedChanges.New),
|
||||
"provenance_invalid_update", len(authorizedChanges.Update),
|
||||
"provenance_invalid_delete", len(authorizedChanges.Delete))
|
||||
"provenance_invalid_add", len(groupChanges.New),
|
||||
"provenance_invalid_update", len(groupChanges.Update),
|
||||
"provenance_invalid_delete", len(groupChanges.Delete))
|
||||
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",
|
||||
"expected", len(authorizedChanges.Delete),
|
||||
"allowed", len(authorizedChanges.Delete))
|
||||
"expected", len(groupChanges.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",
|
||||
"expected", len(authorizedChanges.Update),
|
||||
"allowed", len(authorizedChanges.Update))
|
||||
"expected", len(groupChanges.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))
|
||||
@ -565,6 +558,7 @@ type ruleUpdate struct {
|
||||
|
||||
type changes struct {
|
||||
GroupKey ngmodels.AlertRuleGroupKey
|
||||
AffectedGroups map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule
|
||||
New []*ngmodels.AlertRule
|
||||
Update []ruleUpdate
|
||||
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).
|
||||
// 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) {
|
||||
affectedGroups := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
|
||||
q := &ngmodels.ListAlertRulesQuery{
|
||||
OrgID: groupKey.OrgID,
|
||||
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)
|
||||
}
|
||||
existingGroupRules := q.Result
|
||||
if len(existingGroupRules) > 0 {
|
||||
affectedGroups[groupKey] = existingGroupRules
|
||||
}
|
||||
|
||||
existingGroupRulesUIDs := make(map[string]*ngmodels.AlertRule, len(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 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 {
|
||||
var existing *ngmodels.AlertRule = nil
|
||||
|
||||
if r.UID != "" {
|
||||
if existingGroupRule, ok := existingGroupRulesUIDs[r.UID]; ok {
|
||||
existing = existingGroupRule
|
||||
// remove the rule from existingGroupRulesUIDs
|
||||
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
|
||||
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 {
|
||||
q := &ngmodels.GetAlertRulesGroupByRuleUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
|
||||
if err := ruleStore.GetAlertRulesGroupByRuleUID(ctx, q); err != nil {
|
||||
return nil, fmt.Errorf("failed to query database for a group of alert rules: %w", 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)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to query database for an alert rule with UID %s: %w", r.UID, err)
|
||||
}
|
||||
existing = q.Result
|
||||
affectedGroups[existing.GetGroupKey()] = q.Result
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,6 +645,7 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
|
||||
|
||||
return &changes{
|
||||
GroupKey: groupKey,
|
||||
AffectedGroups: affectedGroups,
|
||||
New: toAdd,
|
||||
Delete: toDelete,
|
||||
Update: toUpdate,
|
||||
|
@ -64,6 +64,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, make([]*models.AlertRule, 0))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, groupKey, changes.GroupKey)
|
||||
require.Empty(t, changes.New)
|
||||
require.Empty(t, changes.Update)
|
||||
require.Len(t, changes.Delete, len(inDatabaseMap))
|
||||
@ -72,6 +73,8 @@ func TestCalculateChanges(t *testing.T) {
|
||||
db := inDatabaseMap[toDelete.UID]
|
||||
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) {
|
||||
@ -85,6 +88,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, groupKey, changes.GroupKey)
|
||||
require.Len(t, changes.Update, len(inDatabase))
|
||||
for _, upsert := range changes.Update {
|
||||
require.NotNil(t, upsert.Existing)
|
||||
@ -95,6 +99,9 @@ func TestCalculateChanges(t *testing.T) {
|
||||
}
|
||||
require.Empty(t, changes.Delete)
|
||||
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) {
|
||||
@ -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) {
|
||||
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.PutRule(context.Background(), inDatabase...)
|
||||
@ -206,6 +214,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
changes, err := calculateChanges(context.Background(), fakeStore, groupKey, submitted)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, groupKey, changes.GroupKey)
|
||||
require.Empty(t, changes.Delete)
|
||||
require.Empty(t, changes.New)
|
||||
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.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) {
|
||||
@ -251,7 +265,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
expectedErr := errors.New("TEST ERROR")
|
||||
fakeStore.Hook = func(cmd interface{}) error {
|
||||
switch cmd.(type) {
|
||||
case models.GetAlertRuleByUIDQuery:
|
||||
case models.GetAlertRulesGroupByRuleUIDQuery:
|
||||
return expectedErr
|
||||
}
|
||||
return nil
|
||||
@ -261,7 +275,7 @@ func TestCalculateChanges(t *testing.T) {
|
||||
submitted := models.AlertRuleGen(withOrgID(orgId), simulateSubmitted)()
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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(change *changes, evaluator func(evaluator ac.Evaluator) bool) (*changes, error) {
|
||||
var result = &changes{
|
||||
GroupKey: change.GroupKey,
|
||||
New: change.New,
|
||||
Update: change.Update,
|
||||
Delete: change.Delete,
|
||||
func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) error {
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
|
||||
|
||||
rules, ok := change.AffectedGroups[change.GroupKey]
|
||||
if ok { // not ok can be when user creates a new rule group or moves existing alerts to a new group
|
||||
if !authorizeAccessToRuleGroup(rules, evaluator) { // if user is not authorized to do operation in the group that is being changed
|
||||
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)
|
||||
}
|
||||
} 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))
|
||||
}
|
||||
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
|
||||
if len(change.Delete) > 0 {
|
||||
var allowedToDelete []*ngmodels.AlertRule
|
||||
for _, rule := range change.Delete {
|
||||
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
|
||||
if dsAllowed {
|
||||
allowedToDelete = append(allowedToDelete, rule)
|
||||
}
|
||||
}
|
||||
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, 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
|
||||
@ -254,12 +263,12 @@ func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator
|
||||
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, change.GroupKey.NamespaceUID)
|
||||
return fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
||||
}
|
||||
for _, rule := range change.New {
|
||||
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
|
||||
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 {
|
||||
dsAllowed := authorizeDatasourceAccessForRule(rule.New, evaluator)
|
||||
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.
|
||||
if rule.Existing.NamespaceUID != rule.New.NamespaceUID {
|
||||
allowed := evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID))))
|
||||
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 {
|
||||
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
|
||||
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
|
||||
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, change.GroupKey.NamespaceUID)
|
||||
if rule.Existing.NamespaceUID != rule.New.NamespaceUID || rule.Existing.RuleGroup != rule.New.RuleGroup {
|
||||
key := rule.Existing.GetGroupKey()
|
||||
rules, ok = change.AffectedGroups[key]
|
||||
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
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
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) {
|
||||
groupKey := models.GenerateGroupKey(rand.Int63())
|
||||
namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
|
||||
@ -103,34 +166,60 @@ 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 {
|
||||
rules := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
rules2 := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
return &changes{
|
||||
GroupKey: groupKey,
|
||||
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
|
||||
groupKey: append(rules, rules2...),
|
||||
},
|
||||
New: nil,
|
||||
Update: nil,
|
||||
Delete: rules2,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
return map[string][]string{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
namespaceIdScope,
|
||||
},
|
||||
datasources.ActionQuery: getDatasourceScopesForRules(c.AffectedGroups[c.GroupKey]),
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if there are rules to update within the same namespace it should check update action and access to datasource",
|
||||
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)))
|
||||
updates := make([]ruleUpdate, 0, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
cp := models.CopyRule(rule)
|
||||
cp.Data = []models.AlertQuery{models.GenerateAlertQuery()}
|
||||
updates = append(updates, ruleUpdate{
|
||||
Existing: rule,
|
||||
New: models.CopyRule(rule),
|
||||
New: cp,
|
||||
Diff: nil,
|
||||
})
|
||||
}
|
||||
|
||||
return &changes{
|
||||
GroupKey: groupKey,
|
||||
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
|
||||
groupKey: append(rules, rules1...),
|
||||
},
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
scopes := getDatasourceScopesForRules(append(c.AffectedGroups[c.GroupKey], mapUpdates(c.Update, func(update ruleUpdate) *models.AlertRule {
|
||||
return update.New
|
||||
})...))
|
||||
return map[string][]string{
|
||||
ac.ActionAlertingRuleUpdate: {
|
||||
namespaceIdScope,
|
||||
@ -140,43 +229,131 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if there are rules that are moved between namespaces it should check update action",
|
||||
name: "if there are rules that are moved between namespaces it should check delete+add action and access to group where rules come from",
|
||||
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)))
|
||||
updates := make([]ruleUpdate, 0, len(rules))
|
||||
|
||||
targetGroupKey := models.GenerateGroupKey(groupKey.OrgID)
|
||||
|
||||
updates := make([]ruleUpdate, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
cp := models.CopyRule(rule)
|
||||
cp.NamespaceUID = rule.NamespaceUID + "other"
|
||||
withGroupKey(targetGroupKey)(cp)
|
||||
cp.Data = []models.AlertQuery{
|
||||
models.GenerateAlertQuery(),
|
||||
}
|
||||
|
||||
updates = append(updates, ruleUpdate{
|
||||
Existing: cp,
|
||||
New: rule,
|
||||
Diff: nil,
|
||||
Existing: rule,
|
||||
New: cp,
|
||||
})
|
||||
}
|
||||
|
||||
return &changes{
|
||||
GroupKey: groupKey,
|
||||
GroupKey: targetGroupKey,
|
||||
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
|
||||
groupKey: append(rules, rules1...),
|
||||
},
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
var scopes []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{
|
||||
ac.ActionAlertingRuleDelete: deleteScopes,
|
||||
ac.ActionAlertingRuleCreate: {
|
||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID),
|
||||
},
|
||||
datasources.ActionQuery: dsScopes,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
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 {
|
||||
targetGroupKey := models.AlertRuleGroupKey{
|
||||
OrgID: groupKey.OrgID,
|
||||
NamespaceUID: groupKey.NamespaceUID,
|
||||
RuleGroup: util.GenerateShortUID(),
|
||||
}
|
||||
sourceGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(groupKey)))
|
||||
targetGroup := models.GenerateAlertRules(rand.Intn(4)+1, models.AlertRuleGen(withGroupKey(targetGroupKey)))
|
||||
|
||||
updates := make([]ruleUpdate, 0, len(sourceGroup))
|
||||
toCopy := len(sourceGroup)
|
||||
if toCopy > 1 {
|
||||
toCopy = rand.Intn(toCopy-1) + 1
|
||||
}
|
||||
for i := 0; i < toCopy; i++ {
|
||||
rule := sourceGroup[0]
|
||||
cp := models.CopyRule(rule)
|
||||
withGroupKey(targetGroupKey)(cp)
|
||||
cp.Data = []models.AlertQuery{
|
||||
models.GenerateAlertQuery(),
|
||||
}
|
||||
|
||||
updates = append(updates, ruleUpdate{
|
||||
Existing: rule,
|
||||
New: cp,
|
||||
})
|
||||
}
|
||||
|
||||
return &changes{
|
||||
GroupKey: targetGroupKey,
|
||||
AffectedGroups: map[models.AlertRuleGroupKey][]*models.AlertRule{
|
||||
groupKey: sourceGroup,
|
||||
targetGroupKey: targetGroup,
|
||||
},
|
||||
New: nil,
|
||||
Update: updates,
|
||||
Delete: nil,
|
||||
}
|
||||
},
|
||||
permissions: func(c *changes) map[string][]string {
|
||||
scopes := make(map[string]struct{})
|
||||
for _, update := range c.Update {
|
||||
for _, query := range update.New.Data {
|
||||
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
|
||||
scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
|
||||
}
|
||||
for _, query := range update.Existing.Data {
|
||||
scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, rules := range c.AffectedGroups {
|
||||
for _, rule := range rules {
|
||||
for _, query := range rule.Data {
|
||||
scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dsScopes := make([]string, 0, len(scopes))
|
||||
for key := range scopes {
|
||||
dsScopes = append(dsScopes, key)
|
||||
}
|
||||
|
||||
return map[string][]string{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID + "other"),
|
||||
ac.ActionAlertingRuleUpdate: {
|
||||
dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID),
|
||||
},
|
||||
ac.ActionAlertingRuleCreate: {
|
||||
namespaceIdScope,
|
||||
},
|
||||
datasources.ActionQuery: scopes,
|
||||
datasources.ActionQuery: dsScopes,
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -184,188 +361,39 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
groupChanges := testCase.changes()
|
||||
permissions := testCase.permissions(groupChanges)
|
||||
|
||||
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
|
||||
})
|
||||
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
|
||||
|
||||
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 {
|
||||
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())
|
||||
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.Equal(t, groupChanges, result)
|
||||
require.Truef(t, executed, "evaluation function is expected to be called but it was not.")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeRuleDelete(t *testing.T) {
|
||||
groupKey := models.GenerateGroupKey(rand.Int63())
|
||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID)
|
||||
|
||||
getScopes := func(rules []*models.AlertRule) []string {
|
||||
var scopes []string
|
||||
for _, rule := range rules {
|
||||
for _, query := range rule.Data {
|
||||
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))
|
||||
}
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
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{
|
||||
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{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
namespaceScope,
|
||||
},
|
||||
datasources.ActionQuery: getScopes(c.Delete),
|
||||
}
|
||||
},
|
||||
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",
|
||||
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{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
namespaceScope,
|
||||
},
|
||||
datasources.ActionQuery: {
|
||||
getScopes(c.Delete[:1])[0],
|
||||
},
|
||||
}
|
||||
},
|
||||
assert: func(t *testing.T, orig, authz *changes, err error) {
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(orig.Delete), len(authz.Delete))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not fail if no changes other than unauthorized",
|
||||
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{
|
||||
ac.ActionAlertingRuleDelete: {
|
||||
namespaceScope,
|
||||
},
|
||||
}
|
||||
},
|
||||
assert: func(t *testing.T, orig, authz *changes, err error) {
|
||||
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),
|
||||
}
|
||||
},
|
||||
assert: func(t *testing.T, _, c *changes, err error) {
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, c.Delete)
|
||||
},
|
||||
},
|
||||
{
|
||||
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),
|
||||
}
|
||||
},
|
||||
assert: func(t *testing.T, _, c *changes, err error) {
|
||||
require.ErrorIs(t, err, ErrAuthorization)
|
||||
require.Nil(t, c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
groupChanges := testCase.changes()
|
||||
permissions := testCase.permissions(groupChanges)
|
||||
result, err := authorizeRuleChanges(groupChanges, func(evaluator ac.Evaluator) bool {
|
||||
response := evaluator.Evaluate(permissions)
|
||||
return response
|
||||
})
|
||||
|
||||
testCase.assert(t, groupChanges, result, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
||||
rule := models.AlertRuleGen()()
|
||||
|
||||
@ -420,3 +448,48 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
type ListAlertRulesQuery struct {
|
||||
OrgID int64
|
||||
|
@ -37,6 +37,7 @@ type RuleStore interface {
|
||||
DeleteAlertRulesByUID(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
|
||||
GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) error
|
||||
GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error
|
||||
ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
|
||||
// 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.
|
||||
func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) 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
|
||||
}
|
||||
|
||||
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.
|
||||
func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models.GetAlertRulesForSchedulingQuery) error {
|
||||
f.mtx.Lock()
|
||||
|
Loading…
Reference in New Issue
Block a user