Alerting: handle folder permissions when fine-grained access enabled (#47035)

* Use alert:create action for folder search with edit permissions. This matches the action that is used to query dashboards (the update will be addressed later)
* Update rule store to use FindDashboards instead of folder service to list folders the user has access to view alerts. Folder service does not support query type and additional filters. 
* Do not check whether the user can save to folder if FGAC is enabled because it is checked on API level.
This commit is contained in:
Yuriy Tseretyan 2022-04-01 19:33:26 -04:00 committed by GitHub
parent 5c3308c6f3
commit 51114527dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 52 additions and 18 deletions

View File

@ -85,7 +85,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
}
namespaceMap, err := srv.store.GetNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser)
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user")
}

View File

@ -196,7 +196,7 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp
}
func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response {
namespaceMap, err := srv.store.GetNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser)
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user")
}

View File

@ -93,6 +93,7 @@ func (ng *AlertNG) init() error {
SQLStore: ng.SQLStore,
Logger: ng.Log,
FolderService: ng.folderService,
AccessControl: ng.accesscontrol,
}
decryptFn := ng.SecretsService.GetDecryptedValue

View File

@ -6,13 +6,12 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/util"
)
@ -42,7 +41,7 @@ type RuleStore interface {
GetOrgAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
GetNamespaceAlertRules(ctx context.Context, query *ngmodels.ListNamespaceAlertRulesQuery) error
GetAlertRules(ctx context.Context, query *ngmodels.GetAlertRulesQuery) error
GetNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error)
GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error)
GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error)
GetOrgRuleGroups(ctx context.Context, query *ngmodels.ListOrgRuleGroupsQuery) error
UpsertAlertRules(ctx context.Context, rule []UpsertRule) error
@ -270,23 +269,44 @@ func (st DBstore) GetAlertRules(ctx context.Context, query *ngmodels.GetAlertRul
})
}
// GetNamespaces returns the folders that are visible to the user
func (st DBstore) GetNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) {
// GetNamespaces returns the folders that are visible to the user and have at least one alert in it
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) {
namespaceMap := make(map[string]*models.Folder)
searchQuery := models.FindPersistedDashboardsQuery{
OrgId: orgID,
SignedInUser: user,
Type: searchstore.TypeAlertFolder,
Limit: -1,
Permission: models.PERMISSION_VIEW,
Sort: models.SortOption{},
Filters: []interface{}{
searchstore.FolderWithAlertsFilter{},
},
}
var page int64 = 1
for {
// if limit is negative; it fetches at most 1000
folders, err := st.FolderService.GetFolders(ctx, user, orgID, -1, page)
query := searchQuery
query.Page = page
proj, err := st.SQLStore.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
if len(folders) == 0 {
if len(proj) == 0 {
break
}
for _, f := range folders {
namespaceMap[f.Uid] = f
for _, hit := range proj {
if !hit.IsFolder {
continue
}
namespaceMap[hit.UID] = &models.Folder{
Id: hit.ID,
Uid: hit.UID,
Title: hit.Title,
}
}
page += 1
}
@ -300,7 +320,8 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, org
return nil, err
}
if withCanSave {
// if access control is disabled, check that the user is allowed to save in the folder.
if withCanSave && st.AccessControl.IsDisabled() {
g := guardian.New(ctx, folder.Id, orgID, user)
if canSave, err := g.CanSave(); err != nil || !canSave {
if err != nil {

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -34,4 +35,5 @@ type DBstore struct {
SQLStore *sqlstore.SQLStore
Logger log.Logger
FolderService dashboards.FolderService
AccessControl accesscontrol.AccessControl
}

View File

@ -208,7 +208,7 @@ func (f *FakeRuleStore) GetAlertRules(_ context.Context, q *models.GetAlertRules
q.Result = result
return nil
}
func (f *FakeRuleStore) GetNamespaces(_ context.Context, orgID int64, _ *models2.SignedInUser) (map[string]*models2.Folder, error) {
func (f *FakeRuleStore) GetUserVisibleNamespaces(_ context.Context, orgID int64, _ *models2.SignedInUser) (map[string]*models2.Folder, error) {
f.mtx.Lock()
defer f.mtx.Unlock()

View File

@ -91,7 +91,7 @@ func NewAccessControlDashboardPermissionFilter(user *models.SignedInUser, permis
if queryType == searchstore.TypeAlertFolder {
folderActions = append(folderActions, accesscontrol.ActionAlertingRuleRead)
if needEdit {
folderActions = append(folderActions, accesscontrol.ActionAlertingRuleUpdate)
folderActions = append(folderActions, accesscontrol.ActionAlertingRuleCreate)
}
} else {
dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsRead)

View File

@ -29,7 +29,7 @@ func TestNewAccessControlDashboardPermissionFilter(t *testing.T) {
expectedFolderActions: []string{
dashboards.ActionFoldersRead,
accesscontrol.ActionAlertingRuleRead,
accesscontrol.ActionAlertingRuleUpdate,
accesscontrol.ActionAlertingRuleCreate,
},
},
{
@ -39,7 +39,7 @@ func TestNewAccessControlDashboardPermissionFilter(t *testing.T) {
expectedFolderActions: []string{
dashboards.ActionFoldersRead,
accesscontrol.ActionAlertingRuleRead,
accesscontrol.ActionAlertingRuleUpdate,
accesscontrol.ActionAlertingRuleCreate,
},
},
{

View File

@ -149,3 +149,13 @@ func sqlIDin(column string, ids []int64) (string, []interface{}) {
}
return fmt.Sprintf("%s IN %s", column, sqlArray), params
}
// FolderWithAlertsFilter applies a filter that makes the result contain only folders that contain alert rules
type FolderWithAlertsFilter struct {
}
var _ FilterWhere = &FolderWithAlertsFilter{}
func (f FolderWithAlertsFilter) Where() (string, []interface{}) {
return "EXISTS (SELECT 1 FROM alert_rule WHERE alert_rule.namespace_uid = dashboard.uid)", nil
}