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 benchmarks
This commit is contained in:
Sofia Papagiannaki
2023-12-15 19:34:08 +02:00
committed by GitHub
parent cf8e8852c3
commit d89a8a3a82
9 changed files with 434 additions and 55 deletions

View File

@@ -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)
})
}

88
pkg/tests/utils.go Normal file
View File

@@ -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)
}
}