mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
2c26a02b82
* Add feature flag * Introduce interface and dummy implementation * Add tests for the new filter * accessControlDashboardPermissionFilterNoFolderSubquery implementation * join only if it's necessary * force ordering for tests * Temporarily enable new query for benchmarks
517 lines
16 KiB
Go
517 lines
16 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
|
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
|
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
|
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
"github.com/grafana/grafana/pkg/services/search"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/star"
|
|
"github.com/grafana/grafana/pkg/services/star/startest"
|
|
"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
|
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
|
"github.com/grafana/grafana/pkg/services/team"
|
|
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
"github.com/grafana/grafana/pkg/web/webtest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
LEVEL0_FOLDER_NUM = 300
|
|
LEVEL1_FOLDER_NUM = 30
|
|
LEVEL2_FOLDER_NUM = 5
|
|
LEVEL0_DASHBOARD_NUM = 300
|
|
LEVEL1_DASHBOARD_NUM = 30
|
|
LEVEL2_DASHBOARD_NUM = 5
|
|
TEAM_NUM = 50
|
|
TEAM_MEMBER_NUM = 5
|
|
|
|
MAXIMUM_INT_POSTGRES = 2147483647
|
|
)
|
|
|
|
type benchScenario struct {
|
|
db *sqlstore.SQLStore
|
|
// signedInUser is the user that is signed in to the server
|
|
cfg *setting.Cfg
|
|
signedInUser *user.SignedInUser
|
|
teamSvc team.Service
|
|
userSvc user.Service
|
|
}
|
|
|
|
func BenchmarkFolderListAndSearch(b *testing.B) {
|
|
start := time.Now()
|
|
b.Log("setup start")
|
|
sc := setupDB(b)
|
|
b.Log("setup time:", time.Since(start))
|
|
|
|
all := LEVEL0_FOLDER_NUM*LEVEL0_DASHBOARD_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL1_DASHBOARD_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM
|
|
|
|
// the maximum number of dashboards that can be returned by the search API
|
|
// otherwise the handler fails with 422 status code
|
|
const limit = 5000
|
|
withLimit := func(res int) int {
|
|
if res > limit {
|
|
return limit
|
|
}
|
|
return res
|
|
}
|
|
|
|
benchmarks := []struct {
|
|
desc string
|
|
url string
|
|
expectedLen int
|
|
features *featuremgmt.FeatureManager
|
|
}{
|
|
{
|
|
desc: "get root folders with nested folders feature enabled",
|
|
url: "/api/folders",
|
|
expectedLen: LEVEL0_FOLDER_NUM,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "get subfolders with nested folders feature enabled",
|
|
url: "/api/folders?parentUid=folder0",
|
|
expectedLen: LEVEL1_FOLDER_NUM,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "list all inherited dashboards with nested folders feature enabled",
|
|
url: "/api/search?type=dash-db&limit=5000",
|
|
expectedLen: withLimit(all),
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "search for pattern with nested folders feature enabled",
|
|
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
|
|
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "search for specific dashboard nested folders feature enabled",
|
|
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
|
|
expectedLen: 1,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "get root folders with nested folders feature disabled",
|
|
url: "/api/folders?limit=5000",
|
|
expectedLen: withLimit(LEVEL0_FOLDER_NUM),
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "list all dashboards with nested folders feature disabled",
|
|
url: "/api/search?type=dash-db&limit=5000",
|
|
expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
{
|
|
desc: "search specific dashboard with nested folders feature disabled",
|
|
url: "/api/search?type=dash-db&query=dashboard_0_0",
|
|
expectedLen: 1,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
|
|
},
|
|
}
|
|
for _, bm := range benchmarks {
|
|
b.Run(bm.desc, func(b *testing.B) {
|
|
m := setupServer(b, sc, bm.features)
|
|
req := httptest.NewRequest(http.MethodGet, bm.url, nil)
|
|
req = webtest.RequestWithSignedInUser(req, sc.signedInUser)
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
rec := httptest.NewRecorder()
|
|
m.ServeHTTP(rec, req)
|
|
require.Equal(b, 200, rec.Code)
|
|
var resp []dtos.FolderSearchHit
|
|
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
require.NoError(b, err)
|
|
assert.Len(b, resp, bm.expectedLen)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func setupDB(b testing.TB) benchScenario {
|
|
b.Helper()
|
|
db := sqlstore.InitTestDB(b)
|
|
IDs := map[int64]struct{}{}
|
|
|
|
opts := sqlstore.NativeSettingsForDialect(db.GetDialect())
|
|
|
|
quotaService := quotatest.New(false, nil)
|
|
cfg := setting.NewCfg()
|
|
|
|
teamSvc := teamimpl.ProvideService(db, cfg)
|
|
orgService, err := orgimpl.ProvideService(db, cfg, quotaService)
|
|
require.NoError(b, err)
|
|
|
|
cache := localcache.ProvideService()
|
|
userSvc, err := userimpl.ProvideService(db, orgService, cfg, teamSvc, cache, "atest.FakeQuotaService{}, bundleregistry.ProvideService())
|
|
require.NoError(b, err)
|
|
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
|
|
|
|
b.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
|
|
var orgID int64 = 1
|
|
|
|
userIDs := make([]int64, 0, TEAM_MEMBER_NUM)
|
|
for i := 0; i < TEAM_MEMBER_NUM; i++ {
|
|
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
|
|
OrgID: orgID,
|
|
Login: fmt.Sprintf("user%d", i),
|
|
})
|
|
require.NoError(b, err)
|
|
require.NotZero(b, u.ID)
|
|
userIDs = append(userIDs, u.ID)
|
|
}
|
|
|
|
signedInUser := user.SignedInUser{UserID: userIDs[0], OrgID: orgID, Permissions: map[int64]map[string][]string{
|
|
orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
|
|
}}
|
|
|
|
now := time.Now()
|
|
roles := make([]accesscontrol.Role, 0, TEAM_NUM)
|
|
teams := make([]team.Team, 0, TEAM_NUM)
|
|
teamMembers := make([]team.TeamMember, 0, TEAM_MEMBER_NUM)
|
|
teamRoles := make([]accesscontrol.TeamRole, 0, TEAM_NUM)
|
|
for i := 1; i < TEAM_NUM+1; i++ {
|
|
teamID := int64(i)
|
|
teams = append(teams, team.Team{
|
|
UID: fmt.Sprintf("team%d", i),
|
|
ID: teamID,
|
|
Name: fmt.Sprintf("team%d", i),
|
|
OrgID: orgID,
|
|
Created: now,
|
|
Updated: now,
|
|
})
|
|
signedInUser.Teams = append(signedInUser.Teams, teamID)
|
|
|
|
for _, userID := range userIDs {
|
|
teamMembers = append(teamMembers, team.TeamMember{
|
|
UserID: userID,
|
|
TeamID: teamID,
|
|
OrgID: orgID,
|
|
Permission: dashboards.PERMISSION_VIEW,
|
|
Created: now,
|
|
Updated: now,
|
|
})
|
|
}
|
|
|
|
name := fmt.Sprintf("managed_team_role_%d", i)
|
|
roles = append(roles, accesscontrol.Role{
|
|
ID: int64(i),
|
|
UID: name,
|
|
OrgID: orgID,
|
|
Name: name,
|
|
Updated: now,
|
|
Created: now,
|
|
})
|
|
|
|
teamRoles = append(teamRoles, accesscontrol.TeamRole{
|
|
RoleID: int64(i),
|
|
OrgID: orgID,
|
|
TeamID: teamID,
|
|
Created: now,
|
|
})
|
|
}
|
|
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
_, err := sess.BulkInsert("team", teams, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("team_member", teamMembers, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("role", roles, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("team_role", teamRoles, opts)
|
|
return err
|
|
})
|
|
require.NoError(b, err)
|
|
|
|
foldersCap := LEVEL0_FOLDER_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM + LEVEL0_FOLDER_NUM*LEVEL1_FOLDER_NUM*LEVEL2_FOLDER_NUM
|
|
folders := make([]*f, 0, foldersCap)
|
|
dashsCap := LEVEL0_FOLDER_NUM * LEVEL1_FOLDER_NUM * LEVEL2_FOLDER_NUM * LEVEL2_DASHBOARD_NUM
|
|
dashs := make([]*dashboards.Dashboard, 0, foldersCap+dashsCap)
|
|
dashTags := make([]*dashboardTag, 0, dashsCap)
|
|
permissions := make([]accesscontrol.Permission, 0, foldersCap*2)
|
|
for i := 0; i < LEVEL0_FOLDER_NUM; i++ {
|
|
f0, d := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d", i), nil)
|
|
folders = append(folders, f0)
|
|
dashs = append(dashs, d)
|
|
|
|
roleID := int64(i%TEAM_NUM + 1)
|
|
permissions = append(permissions, accesscontrol.Permission{
|
|
RoleID: roleID,
|
|
Action: dashboards.ActionFoldersRead,
|
|
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID),
|
|
Updated: now,
|
|
Created: now,
|
|
},
|
|
accesscontrol.Permission{
|
|
RoleID: roleID,
|
|
Action: dashboards.ActionDashboardsRead,
|
|
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f0.UID),
|
|
Updated: now,
|
|
Created: now,
|
|
},
|
|
)
|
|
|
|
for j := 0; j < LEVEL0_DASHBOARD_NUM; j++ {
|
|
str := fmt.Sprintf("dashboard_%d_%d", i, j)
|
|
dashID := generateID(IDs)
|
|
dashs = append(dashs, &dashboards.Dashboard{
|
|
ID: dashID,
|
|
OrgID: signedInUser.OrgID,
|
|
IsFolder: false,
|
|
UID: str,
|
|
FolderID: f0.ID,
|
|
Slug: str,
|
|
Title: str,
|
|
Data: simplejson.New(),
|
|
Created: now,
|
|
Updated: now,
|
|
})
|
|
|
|
dashTags = append(dashTags, &dashboardTag{
|
|
DashboardId: dashID,
|
|
Term: fmt.Sprintf("tag%d", j),
|
|
})
|
|
}
|
|
|
|
for j := 0; j < LEVEL1_FOLDER_NUM; j++ {
|
|
f1, d1 := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d_%d", i, j), &f0.UID)
|
|
folders = append(folders, f1)
|
|
dashs = append(dashs, d1)
|
|
|
|
for k := 0; k < LEVEL1_DASHBOARD_NUM; k++ {
|
|
str := fmt.Sprintf("dashboard_%d_%d_%d", i, j, k)
|
|
dashID := generateID(IDs)
|
|
dashs = append(dashs, &dashboards.Dashboard{
|
|
ID: dashID,
|
|
OrgID: signedInUser.OrgID,
|
|
IsFolder: false,
|
|
UID: str,
|
|
FolderID: f1.ID,
|
|
Slug: str,
|
|
Title: str,
|
|
Data: simplejson.New(),
|
|
Created: now,
|
|
Updated: now,
|
|
})
|
|
|
|
dashTags = append(dashTags, &dashboardTag{
|
|
DashboardId: dashID,
|
|
Term: fmt.Sprintf("tag%d", k),
|
|
})
|
|
}
|
|
|
|
for k := 0; k < LEVEL2_FOLDER_NUM; k++ {
|
|
f2, d2 := addFolder(orgID, generateID(IDs), fmt.Sprintf("folder%d_%d_%d", i, j, k), &f1.UID)
|
|
folders = append(folders, f2)
|
|
dashs = append(dashs, d2)
|
|
|
|
for l := 0; l < LEVEL2_DASHBOARD_NUM; l++ {
|
|
str := fmt.Sprintf("dashboard_%d_%d_%d_%d", i, j, k, l)
|
|
dashID := generateID(IDs)
|
|
dashs = append(dashs, &dashboards.Dashboard{
|
|
ID: dashID,
|
|
OrgID: signedInUser.OrgID,
|
|
IsFolder: false,
|
|
UID: str,
|
|
FolderID: f2.ID,
|
|
Slug: str,
|
|
Title: str,
|
|
Data: simplejson.New(),
|
|
Created: now,
|
|
Updated: now,
|
|
})
|
|
|
|
dashTags = append(dashTags, &dashboardTag{
|
|
DashboardId: dashID,
|
|
Term: fmt.Sprintf("tag%d", l),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
_, err := sess.BulkInsert("folder", folders, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("dashboard", dashs, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("permission", permissions, opts)
|
|
require.NoError(b, err)
|
|
|
|
_, err = sess.BulkInsert("dashboard_tag", dashTags, opts)
|
|
return err
|
|
})
|
|
require.NoError(b, err)
|
|
return benchScenario{
|
|
db: db,
|
|
cfg: cfg,
|
|
signedInUser: &signedInUser,
|
|
teamSvc: teamSvc,
|
|
userSvc: userSvc,
|
|
}
|
|
}
|
|
|
|
func setupServer(b testing.TB, sc benchScenario, features *featuremgmt.FeatureManager) *web.Macaron {
|
|
b.Helper()
|
|
|
|
m := web.New()
|
|
initCtx := &contextmodel.ReqContext{}
|
|
m.Use(func(c *web.Context) {
|
|
initCtx.Context = c
|
|
initCtx.Logger = log.New("api-test")
|
|
initCtx.SignedInUser = sc.signedInUser
|
|
|
|
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
|
|
})
|
|
|
|
license := licensingtest.NewFakeLicensing()
|
|
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
|
|
|
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), features)
|
|
|
|
quotaSrv := quotatest.New(false, nil)
|
|
|
|
dashStore, err := database.ProvideDashboardStore(sc.db, sc.db.Cfg, features, tagimpl.ProvideService(sc.db, sc.db.Cfg), quotaSrv)
|
|
require.NoError(b, err)
|
|
|
|
folderStore := folderimpl.ProvideDashboardFolderStore(sc.db)
|
|
|
|
ac := acimpl.ProvideAccessControl(sc.cfg)
|
|
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features)
|
|
|
|
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
|
features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
|
|
require.NoError(b, err)
|
|
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
|
|
features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
|
|
require.NoError(b, err)
|
|
|
|
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
|
sc.cfg, dashStore, folderStore, nil,
|
|
features, folderPermissions, dashboardPermissions, ac,
|
|
folderServiceWithFlagOn,
|
|
)
|
|
require.NoError(b, err)
|
|
|
|
starSvc := startest.NewStarServiceFake()
|
|
starSvc.ExpectedUserStars = &star.GetUserStarsResult{UserStars: make(map[int64]bool)}
|
|
hs := &HTTPServer{
|
|
CacheService: localcache.New(5*time.Minute, 10*time.Minute),
|
|
Cfg: sc.cfg,
|
|
SQLStore: sc.db,
|
|
Features: features,
|
|
QuotaService: quotaSrv,
|
|
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc),
|
|
folderService: folderServiceWithFlagOn,
|
|
}
|
|
|
|
m.Get("/api/folders", hs.GetFolders)
|
|
m.Get("/api/search", hs.Search)
|
|
|
|
return m
|
|
}
|
|
|
|
type f struct {
|
|
ID int64 `xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `xorm:"org_id"`
|
|
UID string `xorm:"uid"`
|
|
ParentUID *string `xorm:"parent_uid"`
|
|
Title string
|
|
Description string
|
|
|
|
Created time.Time
|
|
Updated time.Time
|
|
}
|
|
|
|
func (f *f) TableName() string {
|
|
return "folder"
|
|
}
|
|
|
|
// SQL bean helper to save tags
|
|
type dashboardTag struct {
|
|
Id int64
|
|
DashboardId int64
|
|
Term string
|
|
}
|
|
|
|
func addFolder(orgID int64, id int64, uid string, parentUID *string) (*f, *dashboards.Dashboard) {
|
|
now := time.Now()
|
|
title := uid
|
|
f := &f{
|
|
OrgID: orgID,
|
|
UID: uid,
|
|
Title: title,
|
|
ID: id,
|
|
Created: now,
|
|
Updated: now,
|
|
ParentUID: parentUID,
|
|
}
|
|
|
|
d := &dashboards.Dashboard{
|
|
ID: id,
|
|
OrgID: orgID,
|
|
UID: uid,
|
|
Version: 1,
|
|
Title: title,
|
|
Data: simplejson.NewFromAny(map[string]interface{}{"schemaVersion": 17, "title": title, "uid": uid, "version": 1}),
|
|
IsFolder: true,
|
|
Created: now,
|
|
Updated: now,
|
|
}
|
|
return f, d
|
|
}
|
|
|
|
func generateID(reserved map[int64]struct{}) int64 {
|
|
n := rand.Int63n(MAXIMUM_INT_POSTGRES)
|
|
if _, existing := reserved[n]; existing {
|
|
return generateID(reserved)
|
|
}
|
|
reserved[n] = struct{}{}
|
|
return n
|
|
}
|