mirror of https://github.com/grafana/grafana
Nested Folders: Fix /api/folders pagination (#79447)
* Nested Folders: Fix /api/folders pagination We used to check access to the root folders after fetching them from the DB with pagination. This fix splits logic for fetching folders in: - fetching subfolders - fetching root folders and refactors the query for the latter so that is filters by folders with permissions * Add tests * Update benchmarkspull/79592/head
parent
cf8e8852c3
commit
d89a8a3a82
@ -0,0 +1,199 @@ |
||||
package folders |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
"runtime" |
||||
"testing" |
||||
|
||||
"github.com/grafana/dskit/concurrency" |
||||
"github.com/grafana/grafana-openapi-client-go/client/folders" |
||||
"github.com/grafana/grafana-openapi-client-go/models" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/folder" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
"github.com/grafana/grafana/pkg/tests" |
||||
"github.com/grafana/grafana/pkg/tests/testinfra" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestGetFolders(t *testing.T) { |
||||
// Setup Grafana and its Database
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ |
||||
DisableLegacyAlerting: true, |
||||
EnableUnifiedAlerting: true, |
||||
DisableAnonymous: true, |
||||
AppModeProduction: true, |
||||
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders}, |
||||
}) |
||||
|
||||
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, p) |
||||
|
||||
orgID := int64(1) |
||||
|
||||
// Create a users to make authenticated requests
|
||||
tests.CreateUser(t, store, user.CreateUserCommand{ |
||||
DefaultOrgRole: string(org.RoleViewer), |
||||
OrgID: orgID, |
||||
Password: "viewer", |
||||
Login: "viewer", |
||||
}) |
||||
tests.CreateUser(t, store, user.CreateUserCommand{ |
||||
OrgID: orgID, |
||||
DefaultOrgRole: string(org.RoleEditor), |
||||
Password: "editor", |
||||
Login: "editor", |
||||
}) |
||||
tests.CreateUser(t, store, user.CreateUserCommand{ |
||||
OrgID: orgID, |
||||
DefaultOrgRole: string(org.RoleAdmin), |
||||
Password: "admin", |
||||
Login: "admin", |
||||
}) |
||||
|
||||
adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") |
||||
editorClient := tests.GetClient(grafanaListedAddr, "editor", "editor") |
||||
viewerClient := tests.GetClient(grafanaListedAddr, "viewer", "viewer") |
||||
|
||||
// access control permissions store
|
||||
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures()) |
||||
|
||||
numberOfFolders := 5 |
||||
indexWithoutPermission := 3 |
||||
err := concurrency.ForEachJob(context.Background(), numberOfFolders, runtime.NumCPU(), func(_ context.Context, job int) error { |
||||
resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ |
||||
Title: fmt.Sprintf("Folder %d", job), |
||||
UID: fmt.Sprintf("folder-%d", job), |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
require.Equal(t, http.StatusOK, resp.Code()) |
||||
if job == indexWithoutPermission { |
||||
tests.RemoveFolderPermission(t, permissionsStore, orgID, org.RoleViewer, resp.Payload.UID) |
||||
t.Log("Removed viewer permission from folder", resp.Payload.UID) |
||||
} |
||||
return nil |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
t.Run("Admin can get all folders", func(t *testing.T) { |
||||
res, err := adminClient.Folders.GetFolders(folders.NewGetFoldersParams()) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1", "folder-2", "folder-3", "folder-4"}, actualFolders) |
||||
}) |
||||
|
||||
t.Run("Pagination works as expect for admin", func(t *testing.T) { |
||||
limit := int64(2) |
||||
page := int64(1) |
||||
res, err := adminClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1"}, actualFolders) |
||||
|
||||
page = int64(2) |
||||
res, err = adminClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders = make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-2", "folder-3"}, actualFolders) |
||||
|
||||
page = int64(3) |
||||
res, err = adminClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders = make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-4"}, actualFolders) |
||||
}) |
||||
|
||||
t.Run("Editor can get all folders", func(t *testing.T) { |
||||
res, err := editorClient.Folders.GetFolders(folders.NewGetFoldersParams()) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1", "folder-2", "folder-3", "folder-4", folder.SharedWithMeFolderUID}, actualFolders) |
||||
}) |
||||
|
||||
t.Run("Pagination works as expect for editor", func(t *testing.T) { |
||||
limit := int64(2) |
||||
page := int64(1) |
||||
res, err := editorClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1", folder.SharedWithMeFolderUID}, actualFolders) |
||||
|
||||
page = int64(2) |
||||
res, err = editorClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders = make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-2", "folder-3"}, actualFolders) |
||||
|
||||
page = int64(3) |
||||
res, err = editorClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders = make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-4"}, actualFolders) |
||||
}) |
||||
|
||||
t.Run("Viewer can get only the folders has access too", func(t *testing.T) { |
||||
res, err := viewerClient.Folders.GetFolders(folders.NewGetFoldersParams()) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1", "folder-2", "folder-4", folder.SharedWithMeFolderUID}, actualFolders) |
||||
}) |
||||
|
||||
t.Run("Pagination works as expect for viewer", func(t *testing.T) { |
||||
limit := int64(2) |
||||
page := int64(1) |
||||
res, err := viewerClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders := make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-0", "folder-1", folder.SharedWithMeFolderUID}, actualFolders) |
||||
|
||||
page = int64(2) |
||||
res, err = viewerClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
actualFolders = make([]string, 0, len(res.Payload)) |
||||
for i := range res.Payload { |
||||
actualFolders = append(actualFolders, res.Payload[i].UID) |
||||
} |
||||
assert.Equal(t, []string{"folder-2", "folder-4"}, actualFolders) |
||||
|
||||
page = int64(3) |
||||
res, err = viewerClient.Folders.GetFolders(folders.NewGetFoldersParams().WithLimit(&limit).WithPage(&page)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.Payload, 0) |
||||
}) |
||||
} |
@ -0,0 +1,88 @@ |
||||
package tests |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"net/url" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/go-openapi/strfmt" |
||||
goapi "github.com/grafana/grafana-openapi-client-go/client" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl" |
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
"github.com/grafana/grafana/pkg/services/user/userimpl" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func CreateUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserCommand) int64 { |
||||
t.Helper() |
||||
|
||||
store.Cfg.AutoAssignOrg = true |
||||
store.Cfg.AutoAssignOrgId = 1 |
||||
|
||||
quotaService := quotaimpl.ProvideService(store, store.Cfg) |
||||
orgService, err := orgimpl.ProvideService(store, store.Cfg, quotaService) |
||||
require.NoError(t, err) |
||||
usrSvc, err := userimpl.ProvideService(store, orgService, store.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService()) |
||||
require.NoError(t, err) |
||||
|
||||
u, err := usrSvc.Create(context.Background(), &cmd) |
||||
require.NoError(t, err) |
||||
return u.ID |
||||
} |
||||
|
||||
func GetClient(host string, username string, password string) *goapi.GrafanaHTTPAPI { |
||||
cfg := &goapi.TransportConfig{ |
||||
// Host is the doman name or IP address of the host that serves the API.
|
||||
Host: host, |
||||
// BasePath is the URL prefix for all API paths, relative to the host root.
|
||||
BasePath: "/api", |
||||
// Schemes are the transfer protocols used by the API (http or https).
|
||||
Schemes: []string{"http"}, |
||||
// APIKey is an optional API key or service account token.
|
||||
APIKey: os.Getenv("API_ACCESS_TOKEN"), |
||||
// BasicAuth is optional basic auth credentials.
|
||||
BasicAuth: url.UserPassword(username, password), |
||||
// OrgID provides an optional organization ID.
|
||||
// OrgID is only supported with BasicAuth since API keys are already org-scoped.
|
||||
OrgID: 1, |
||||
// TLSConfig provides an optional configuration for a TLS client
|
||||
TLSConfig: &tls.Config{}, |
||||
// NumRetries contains the optional number of attempted retries
|
||||
NumRetries: 3, |
||||
// RetryTimeout sets an optional time to wait before retrying a request
|
||||
RetryTimeout: 0, |
||||
// RetryStatusCodes contains the optional list of status codes to retry
|
||||
// Use "x" as a wildcard for a single digit (default: [429, 5xx])
|
||||
RetryStatusCodes: []string{"420", "5xx"}, |
||||
// HTTPHeaders contains an optional map of HTTP headers to add to each request
|
||||
HTTPHeaders: map[string]string{}, |
||||
} |
||||
return goapi.NewHTTPClientWithConfig(strfmt.Default, cfg) |
||||
} |
||||
|
||||
func RemoveFolderPermission(t *testing.T, store resourcepermissions.Store, orgID int64, role org.RoleType, uid string) { |
||||
t.Helper() |
||||
|
||||
// remove org role permissions from folder
|
||||
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(role), resourcepermissions.SetResourcePermissionCommand{ |
||||
Resource: "folders", |
||||
ResourceID: uid, |
||||
ResourceAttribute: "uid", |
||||
}, nil) |
||||
|
||||
// remove org role children permissions from folder
|
||||
for _, c := range role.Children() { |
||||
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(c), resourcepermissions.SetResourcePermissionCommand{ |
||||
Resource: "folders", |
||||
ResourceID: uid, |
||||
ResourceAttribute: "uid", |
||||
}, nil) |
||||
} |
||||
} |
Loading…
Reference in new issue