From 81089b956abfdab3ecf29df86db3442a45a29349 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Fri, 17 Jun 2022 13:55:31 -0400 Subject: [PATCH] Alerting: Update authorization rules for RouteGetNamespaceRulesConfig (#50965) * use authorizeAccessToRuleGroup * use toGettableRuleGroupConfig in get by namespace * add comments for controller methods --- pkg/services/ngalert/api/api_ruler.go | 41 ++++------- pkg/services/ngalert/api/api_ruler_test.go | 84 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 28 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index b792670c8b1..b2f69167225 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -157,6 +157,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons return response.JSON(http.StatusAccepted, util.DynMap{"message": "rules deleted"}) } +// RouteGetNamespaceRulesConfig returns all rules in a specific folder that user has access to func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { namespaceTitle := web.Params(c.Req)[":Namespace"] namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) @@ -173,7 +174,6 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. } result := apimodels.NamespaceConfigResponse{} - ruleGroupConfigs := make(map[string]apimodels.GettableRuleGroupConfig) hasAccess := func(evaluator accesscontrol.Evaluator) bool { return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqViewer, evaluator) @@ -184,33 +184,23 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group") } + ruleGroups := make(map[string][]*ngmodels.AlertRule) for _, r := range q.Result { - if !authorizeDatasourceAccessForRule(r, hasAccess) { - continue - } - ruleGroupConfig, ok := ruleGroupConfigs[r.RuleGroup] - if !ok { - ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - ruleGroupConfigs[r.RuleGroup] = apimodels.GettableRuleGroupConfig{ - Name: r.RuleGroup, - Interval: ruleGroupInterval, - Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords), - }, - } - } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) - ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig - } + ruleGroups[r.RuleGroup] = append(ruleGroups[r.RuleGroup], r) } - for _, ruleGroupConfig := range ruleGroupConfigs { - result[namespaceTitle] = append(result[namespaceTitle], ruleGroupConfig) + for groupName, rules := range ruleGroups { + if !authorizeAccessToRuleGroup(rules, hasAccess) { + continue + } + result[namespaceTitle] = append(result[namespaceTitle], toGettableRuleGroupConfig(groupName, rules, namespace.Id, provenanceRecords)) } return response.JSON(http.StatusAccepted, result) } +// RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder). +// If user does not have access to at least one of the rule in the group, returns status 401 Unauthorized func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Response { namespaceTitle := web.Params(c.Req)[":Namespace"] namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) @@ -237,20 +227,17 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Resp return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") } - groupRules := make([]*ngmodels.AlertRule, 0, len(q.Result)) - for _, r := range q.Result { - if !authorizeDatasourceAccessForRule(r, hasAccess) { - 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) + if !authorizeAccessToRuleGroup(q.Result, hasAccess) { + 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), "") } result := apimodels.RuleGroupConfigResponse{ - GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, groupRules, namespace.Id, provenanceRecords), + GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, q.Result, namespace.Id, provenanceRecords), } return response.JSON(http.StatusAccepted, result) } +// RouteGetRulesConfig returns all alert rules that are available to the current user func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser) if err != nil { diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 65ec3ed0228..7228bb17cdb 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -6,6 +6,7 @@ import ( "errors" "math/rand" "net/http" + "net/url" "testing" "time" @@ -659,6 +660,84 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { }) } +func TestRouteGetRulesConfig(t *testing.T) { + t.Run("fine-grained access is enabled", func(t *testing.T) { + t.Run("should check access to data source", func(t *testing.T) { + orgID := rand.Int63() + ruleStore := store.NewFakeRuleStore(t) + folder1 := randFolder() + folder2 := randFolder() + ruleStore.Folders[orgID] = []*models2.Folder{folder1, folder2} + + group1Key := models.GenerateGroupKey(orgID) + group1Key.NamespaceUID = folder1.Uid + group2Key := models.GenerateGroupKey(orgID) + group2Key.NamespaceUID = folder2.Uid + + group1 := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(group1Key))) + group2 := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(group2Key))) + ruleStore.PutRule(context.Background(), append(group1, group2...)...) + + request := createRequestContext(orgID, "", nil) + t.Run("and do not return group if user does not have access to one of rules", func(t *testing.T) { + ac := acMock.New().WithPermissions(createPermissionsForRules(append(group1, group2[1:]...))) + response := createService(ac, ruleStore, nil).RouteGetRulesConfig(request) + require.Equal(t, http.StatusOK, response.Status()) + + result := &apimodels.NamespaceConfigResponse{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + + require.Contains(t, *result, folder1.Title) + require.NotContains(t, *result, folder2.Title) + + groups := (*result)[folder1.Title] + require.Len(t, groups, 1) + require.Equal(t, group1Key.RuleGroup, groups[0].Name) + require.Len(t, groups[0].Rules, len(group1)) + }) + }) + }) +} + +func TestRouteGetRulesGroupConfig(t *testing.T) { + t.Run("fine-grained access is enabled", func(t *testing.T) { + t.Run("should check access to data source", func(t *testing.T) { + orgID := rand.Int63() + folder := randFolder() + ruleStore := store.NewFakeRuleStore(t) + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) + groupKey := models.GenerateGroupKey(orgID) + groupKey.NamespaceUID = folder.Uid + + expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey))) + ruleStore.PutRule(context.Background(), expectedRules...) + + request := createRequestContext(orgID, "", map[string]string{ + ":Namespace": folder.Title, + ":Groupname": groupKey.RuleGroup, + }) + + t.Run("and return 401 if user does not have access one of rules", func(t *testing.T) { + ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules[1:])) + response := createService(ac, ruleStore, nil).RouteGetRulesGroupConfig(request) + require.Equal(t, http.StatusUnauthorized, response.Status()) + }) + + t.Run("and return rules if user has access to all of them", func(t *testing.T) { + ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules)) + response := createService(ac, ruleStore, nil).RouteGetRulesGroupConfig(request) + + require.Equal(t, http.StatusAccepted, response.Status()) + result := &apimodels.RuleGroupConfigResponse{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + require.Len(t, result.Rules, len(expectedRules)) + }) + }) + }) +} + func TestVerifyProvisionedRulesNotAffected(t *testing.T) { orgID := rand.Int63() group := models.GenerateGroupKey(orgID) @@ -735,7 +814,10 @@ func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedu } func createRequestContext(orgID int64, role models2.RoleType, params map[string]string) *models2.ReqContext { - ctx := web.Context{Req: &http.Request{}} + uri, _ := url.Parse("http://localhost") + ctx := web.Context{Req: &http.Request{ + URL: uri, + }} ctx.Req = web.SetURLParams(ctx.Req, params) return &models2.ReqContext{