mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search v1: Add support for inherited folder permissions if nested folders are enabled (#63275)
* Add features dependency to SQLBuilder * Add features dependency to AccessControlDashboardPermissionFilter * Add test for folder inheritance * Dashboard permissions: Return recursive query * Recursive query for inherited folders * Modify search builder * Adjust db.SQLBuilder * Pass flag to SQLbuilder if CTEs are supported * Add support for mysql < 8.0 * Add benchmarking for search with nested folders * Set features to AlertStore * Update pkg/infra/db/sqlbuilder.go Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Set features to LibraryElementService * SQLBuilder tests with nested folder flag set * Apply suggestion from code review Co-authored-by: IevaVasiljeva <ieva.vasiljeva@grafana.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
committed by
GitHub
parent
2648fcb833
commit
988a120d6d
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
)
|
||||
@@ -103,8 +104,14 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
|
||||
queryResult = true
|
||||
return queryResult, nil
|
||||
}
|
||||
err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
|
||||
|
||||
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
}
|
||||
|
||||
err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
|
||||
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?",
|
||||
query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true))
|
||||
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_EDIT)
|
||||
@@ -131,13 +138,18 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
|
||||
|
||||
func (d *dashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) {
|
||||
var queryResult bool
|
||||
err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
}
|
||||
|
||||
err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
if query.SignedInUser.HasRole(org.RoleAdmin) {
|
||||
queryResult = true
|
||||
return nil
|
||||
}
|
||||
|
||||
builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
|
||||
builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
|
||||
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ?", query.SignedInUser.OrgID)
|
||||
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_ADMIN)
|
||||
|
||||
|
||||
@@ -934,9 +934,14 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
||||
}
|
||||
|
||||
if !ac.IsDisabled(d.cfg) {
|
||||
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if access control is enabled, overwrite the filters so far
|
||||
filters = []interface{}{
|
||||
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type),
|
||||
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,33 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
)
|
||||
|
||||
var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
|
||||
@@ -36,7 +48,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
|
||||
sqlStore.Cfg.RBACEnabled = false
|
||||
quotaService := quotatest.New(false, nil)
|
||||
var err error
|
||||
dashboardStore, err = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
||||
dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
||||
require.NoError(t, err)
|
||||
flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp")
|
||||
@@ -477,6 +489,216 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
// the maximux nested folder hierarchy starting from parent down to subfolders
|
||||
nestedFolders := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth+1)
|
||||
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
const (
|
||||
dashInRootTitle = "dashboard in root"
|
||||
dashInParentTitle = "dashboard in parent"
|
||||
dashInSubfolderTitle = "dashboard in subfolder"
|
||||
)
|
||||
var viewer user.SignedInUser
|
||||
var role *accesscontrol.Role
|
||||
|
||||
setup := func() {
|
||||
sqlStore = db.InitTestDB(t)
|
||||
sqlStore.Cfg.RBACEnabled = true
|
||||
quotaService := quotatest.New(false, nil)
|
||||
|
||||
// enable nested folders so that the folder table is populated for all the tests
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||
|
||||
var err error
|
||||
dashboardWriteStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
usr := createUser(t, sqlStore, "viewer", "Viewer", false)
|
||||
viewer = user.SignedInUser{
|
||||
UserID: usr.ID,
|
||||
OrgID: usr.OrgID,
|
||||
OrgRole: org.RoleViewer,
|
||||
}
|
||||
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService())
|
||||
require.NoError(t, err)
|
||||
|
||||
// create admin user in the same org
|
||||
currentUserCmd := user.CreateUserCommand{Login: "admin", Email: "admin@test.com", Name: "an admin", IsAdmin: false, OrgID: viewer.OrgID}
|
||||
u, err := usrSvc.Create(context.Background(), ¤tUserCmd)
|
||||
require.NoError(t, err)
|
||||
admin := user.SignedInUser{
|
||||
UserID: u.ID,
|
||||
OrgID: u.OrgID,
|
||||
OrgRole: org.RoleAdmin,
|
||||
Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||
{
|
||||
Action: dashboards.ActionFoldersCreate,
|
||||
}, {
|
||||
Action: dashboards.ActionFoldersWrite,
|
||||
Scope: dashboards.ScopeFoldersAll,
|
||||
}}),
|
||||
},
|
||||
}
|
||||
require.NotEqual(t, viewer.UserID, admin.UserID)
|
||||
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
})
|
||||
|
||||
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features)
|
||||
|
||||
parentUID := ""
|
||||
for i := 0; ; i++ {
|
||||
uid := fmt.Sprintf("f%d", i)
|
||||
f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
UID: uid,
|
||||
OrgID: admin.OrgID,
|
||||
Title: uid,
|
||||
SignedInUser: &admin,
|
||||
ParentUID: parentUID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, folder.ErrMaximumDepthReached) {
|
||||
break
|
||||
}
|
||||
|
||||
t.Log("unexpected error", "error", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
nestedFolders = append(nestedFolders, f)
|
||||
|
||||
parentUID = f.UID
|
||||
}
|
||||
require.LessOrEqual(t, 2, len(nestedFolders))
|
||||
|
||||
saveDashboardCmd := dashboards.SaveDashboardCommand{
|
||||
UserID: admin.UserID,
|
||||
OrgID: admin.OrgID,
|
||||
IsFolder: false,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": dashInRootTitle,
|
||||
}),
|
||||
}
|
||||
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveDashboardCmd = dashboards.SaveDashboardCommand{
|
||||
UserID: admin.UserID,
|
||||
OrgID: admin.OrgID,
|
||||
IsFolder: false,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": dashInParentTitle,
|
||||
}),
|
||||
FolderID: nestedFolders[0].ID,
|
||||
FolderUID: nestedFolders[0].UID,
|
||||
}
|
||||
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveDashboardCmd = dashboards.SaveDashboardCommand{
|
||||
UserID: admin.UserID,
|
||||
OrgID: admin.OrgID,
|
||||
IsFolder: false,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": dashInSubfolderTitle,
|
||||
}),
|
||||
FolderID: nestedFolders[1].ID,
|
||||
FolderUID: nestedFolders[1].UID,
|
||||
}
|
||||
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
role = setupRBACRole(t, *sqlStore, &viewer)
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
nestedFolderTitles := make([]string, 0, len(nestedFolders))
|
||||
for _, f := range nestedFolders {
|
||||
nestedFolderTitles = append(nestedFolderTitles, f.Title)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
features featuremgmt.FeatureToggles
|
||||
permissions map[string][]string
|
||||
expectedTitles []string
|
||||
}{
|
||||
{
|
||||
desc: "it should not return folder if ACL is not set for parent folder",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
permissions: nil,
|
||||
expectedTitles: nil,
|
||||
},
|
||||
{
|
||||
desc: "it should not return dashboard in subfolder if nested folders are disabled and the user has permission to read dashboards under parent folder",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
permissions: map[string][]string{
|
||||
dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||
},
|
||||
expectedTitles: []string{dashInParentTitle},
|
||||
},
|
||||
{
|
||||
desc: "it should return dashboard in subfolder if nested folders are enabled and the user has permission to read dashboards under parent folder",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
|
||||
permissions: map[string][]string{
|
||||
dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||
},
|
||||
expectedTitles: []string{dashInParentTitle, dashInSubfolderTitle},
|
||||
},
|
||||
{
|
||||
desc: "it should not return subfolder if nested folders are disabled and the user has permission to read folders under parent folder",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
permissions: map[string][]string{
|
||||
dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||
},
|
||||
expectedTitles: []string{nestedFolders[0].Title},
|
||||
},
|
||||
{
|
||||
desc: "it should return subfolder if nested folders are enabled and the user has permission to read folders under parent folder",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
|
||||
permissions: map[string][]string{
|
||||
dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||
},
|
||||
expectedTitles: nestedFolderTitles,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
dashboardReadStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, tc.features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
viewer.Permissions = map[int64]map[string][]string{viewer.OrgID: tc.permissions}
|
||||
setupRBACPermission(t, *sqlStore, role, &viewer)
|
||||
|
||||
query := &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &viewer,
|
||||
OrgId: viewer.OrgID,
|
||||
}
|
||||
|
||||
res, err := testSearchDashboards(dashboardReadStore, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(tc.expectedTitles), len(res))
|
||||
for i, tlt := range tc.expectedTitles {
|
||||
assert.Equal(t, tlt, res[i].Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json,
|
||||
newFolderId int64) *dashboards.Dashboard {
|
||||
t.Helper()
|
||||
@@ -492,3 +714,61 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
func setupRBACRole(t *testing.T, db sqlstore.SQLStore, user *user.SignedInUser) *accesscontrol.Role {
|
||||
t.Helper()
|
||||
var role *accesscontrol.Role
|
||||
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
role = &accesscontrol.Role{
|
||||
OrgID: user.OrgID,
|
||||
UID: "test_role",
|
||||
Name: "test:role",
|
||||
Updated: time.Now(),
|
||||
Created: time.Now(),
|
||||
}
|
||||
_, err := sess.Insert(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Insert(accesscontrol.UserRole{
|
||||
OrgID: role.OrgID,
|
||||
RoleID: role.ID,
|
||||
UserID: user.UserID,
|
||||
Created: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
return role
|
||||
}
|
||||
|
||||
func setupRBACPermission(t *testing.T, db sqlstore.SQLStore, role *accesscontrol.Role, user *user.SignedInUser) {
|
||||
t.Helper()
|
||||
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var acPermission []accesscontrol.Permission
|
||||
for action, scopes := range user.Permissions[user.OrgID] {
|
||||
for _, scope := range scopes {
|
||||
acPermission = append(acPermission, accesscontrol.Permission{
|
||||
RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := sess.InsertMulti(&acPermission); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user