RBAC: Rewrite search and plugin list rbac test (#63483)

* API: rewrite rbac tests for search

* API: rewrite rbac tests for listing plugins

* API: remove unused rbac test setup code
This commit is contained in:
Karl Persson 2023-02-21 11:13:35 +01:00 committed by GitHub
parent 984e293d60
commit 5eaaf9b9b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 275 deletions

View File

@ -19,29 +19,20 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/licensing"
@ -50,21 +41,13 @@ import (
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/preference/preftest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"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/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
@ -255,44 +238,10 @@ func (s *fakeRenderService) Init() error {
return nil
}
// accessControlScenarioContext contains the setups for accesscontrol tests
type accessControlScenarioContext struct {
// server we registered hs routes on.
server *web.Mux
// initCtx is used in a middleware to set the initial context
// of the request server side. Can be used to pretend sign in.
initCtx *contextmodel.ReqContext
// hs is a minimal HTTPServer for the accesscontrol tests to pass.
hs *HTTPServer
// acmock is an accesscontrol mock used to fake users rights.
acmock *accesscontrolmock.Mock
// db is a test database initialized with InitTestDB
db *sqlstore.SQLStore
// cfg is the setting provider
cfg *setting.Cfg
dashboardsStore dashboards.Store
teamService team.Service
userService user.Service
folderPermissionsService *accesscontrolmock.MockPermissionsService
dashboardPermissionsService *accesscontrolmock.MockPermissionsService
}
func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
}
// setInitCtxSignedInUser sets a copy of the user in initCtx
func setInitCtxSignedInUser(initCtx *contextmodel.ReqContext, user user.SignedInUser) {
initCtx.IsSignedIn = true
initCtx.SignedInUser = &user
}
func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
if features == nil {
features = featuremgmt.WithFeatures()
@ -313,140 +262,6 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
}
}
func setupHTTPServer(t *testing.T, useFakeAccessControl bool, options ...APITestServerOption) accessControlScenarioContext {
return setupHTTPServerWithCfg(t, useFakeAccessControl, setting.NewCfg(), options...)
}
func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl bool, cfg *setting.Cfg, options ...APITestServerOption) accessControlScenarioContext {
db := db.InitTestDB(t, db.InitTestDBOpt{})
return setupHTTPServerWithCfgDb(t, useFakeAccessControl, cfg, db, db, featuremgmt.WithFeatures(), options...)
}
func setupHTTPServerWithCfgDb(
t *testing.T, useFakeAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore,
store db.DB, features *featuremgmt.FeatureManager, options ...APITestServerOption,
) accessControlScenarioContext {
t.Helper()
license := &licensing.OSSLicensingService{}
routeRegister := routing.NewRouteRegister()
teamService := teamimpl.ProvideService(db, cfg)
cfg.IsFeatureToggleEnabled = features.IsEnabled
quotaService := quotatest.New(false, nil)
dashboardsStore, err := dashboardsstore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, cfg), quotaService)
require.NoError(t, err)
var acmock *accesscontrolmock.Mock
var ac accesscontrol.AccessControl
var acService accesscontrol.Service
var userSvc user.Service
userMock := usertest.NewUserServiceFake()
userMock.ExpectedUser = &user.User{ID: 1}
orgMock := orgtest.NewOrgServiceFake()
orgMock.ExpectedOrg = &org.Org{}
orgMock.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{}
// Defining the accesscontrol service has to be done before registering routes
if useFakeAccessControl {
acmock = accesscontrolmock.New()
if !cfg.RBACEnabled {
acmock = acmock.WithDisabled()
}
ac = acmock
acService = acmock
userSvc = userMock
} else {
var err error
ac = acimpl.ProvideAccessControl(cfg)
userSvc, err = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService())
require.NoError(t, err)
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), ac, featuremgmt.WithFeatures())
require.NoError(t, err)
}
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService, teamService, userSvc)
require.NoError(t, err)
folderPermissionsService := accesscontrolmock.NewMockedPermissionsService()
dashboardPermissionsService := accesscontrolmock.NewMockedPermissionsService()
folderSvc := foldertest.NewFakeService()
// Create minimal HTTP Server
hs := &HTTPServer{
Cfg: cfg,
Features: features,
Live: newTestLive(t, db),
QuotaService: quotaService,
RouteRegister: routeRegister,
SQLStore: store,
License: &licensing.OSSLicensingService{},
AccessControl: ac,
accesscontrolService: acService,
teamPermissionsService: teamPermissionService,
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
DashboardService: dashboardservice.ProvideDashboardService(
cfg, dashboardsStore, folderimpl.ProvideDashboardFolderStore(db), nil, features,
folderPermissionsService, dashboardPermissionsService, ac,
folderSvc,
),
preferenceService: preftest.NewPreferenceServiceFake(),
userService: userSvc,
orgService: orgMock,
teamService: teamService,
annotationsRepo: annotationstest.NewFakeAnnotationsRepo(),
authInfoService: &logintest.AuthInfoServiceFake{
ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)},
},
}
for _, o := range options {
o(hs)
}
require.NoError(t, hs.declareFixedRoles())
require.NoError(t, hs.accesscontrolService.(accesscontrol.RoleRegistry).RegisterFixedRoles(context.Background()))
// Instantiate a new Server
m := web.New()
// middleware to set the test initial context
initCtx := &contextmodel.ReqContext{}
m.Use(func(c *web.Context) {
initCtx.Context = c
initCtx.Logger = log.New("api-test")
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
})
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService))
// Register all routes
hs.registerRoutes()
hs.RouteRegister.Register(m.Router)
return accessControlScenarioContext{
server: m,
initCtx: initCtx,
hs: hs,
acmock: acmock,
db: db,
cfg: cfg,
dashboardsStore: dashboardsStore,
teamService: teamService,
userService: userSvc,
dashboardPermissionsService: dashboardPermissionsService,
folderPermissionsService: folderPermissionsService,
}
}
func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) *httptest.ResponseRecorder {
req, err := http.NewRequest(method, path, body)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, req)
return recorder
}
func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b))

View File

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
@ -586,56 +587,45 @@ func Test_PluginsList_AccessControl(t *testing.T) {
}
type testCase struct {
desc string
permissions []ac.Permission
expectedCode int
role org.RoleType
isGrafanaAdmin bool
expectedPlugins []string
filters map[string]string
}
tcs := []testCase{
{expectedCode: http.StatusOK, role: org.RoleViewer, expectedPlugins: []string{"mysql"}},
{expectedCode: http.StatusOK, role: org.RoleViewer, isGrafanaAdmin: true, expectedPlugins: []string{"mysql", "test-app"}},
{expectedCode: http.StatusOK, role: org.RoleAdmin, expectedPlugins: []string{"mysql", "test-app"}},
}
testName := func(tc testCase) string {
return fmt.Sprintf("List request returns %d when role: %s, isGrafanaAdmin: %t, filters: %v",
tc.expectedCode, tc.role, tc.isGrafanaAdmin, tc.filters)
}
testUser := func(role org.RoleType, isGrafanaAdmin bool) user.SignedInUser {
return user.SignedInUser{
UserID: 2,
OrgID: 2,
OrgName: "TestOrg2",
OrgRole: role,
Login: "testUser",
Name: "testUser",
Email: "testUser@example.org",
OrgCount: 1,
IsGrafanaAdmin: isGrafanaAdmin,
IsAnonymous: false,
}
{
desc: "should only be able to list core plugins",
permissions: []ac.Permission{},
expectedCode: http.StatusOK,
expectedPlugins: []string{"mysql"},
},
{
desc: "should be able to list core plugins and plugins user has permission to",
permissions: []ac.Permission{{Action: plugins.ActionWrite, Scope: "plugins:id:test-app"}},
expectedCode: http.StatusOK,
expectedPlugins: []string{"mysql", "test-app"},
},
}
for _, tc := range tcs {
sc := setupHTTPServer(t, true)
sc.hs.PluginSettings = &pluginSettings
sc.hs.pluginStore = pluginStore
sc.hs.pluginsUpdateChecker = updatechecker.ProvidePluginsService(sc.hs.Cfg, pluginStore)
setInitCtxSignedInUser(sc.initCtx, testUser(tc.role, tc.isGrafanaAdmin))
t.Run(tc.desc, func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.PluginSettings = &pluginSettings
hs.pluginStore = pluginStore
hs.pluginsUpdateChecker = updatechecker.ProvidePluginsService(hs.Cfg, pluginStore)
})
t.Run(testName(tc), func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, "/api/plugins/", nil, t)
require.Equal(t, tc.expectedCode, response.Code)
var res dtos.PluginList
err := json.NewDecoder(response.Body).Decode(&res)
res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/plugins"), userWithPermissions(1, tc.permissions)))
require.NoError(t, err)
require.Len(t, res, len(tc.expectedPlugins))
for _, plugin := range res {
var result dtos.PluginList
require.NoError(t, json.NewDecoder(res.Body).Decode(&result))
require.Len(t, result, len(tc.expectedPlugins))
for _, plugin := range result {
require.Contains(t, tc.expectedPlugins, plugin.Id)
}
assert.Equal(t, tc.expectedCode, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
}

View File

@ -1,9 +1,7 @@
package api
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@ -12,29 +10,16 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestHTTPServer_Search(t *testing.T) {
sc := setupHTTPServer(t, true)
sc.initCtx.IsSignedIn = true
sc.initCtx.SignedInUser = &user.SignedInUser{}
sc.hs.SearchService = &mockSearchService{
ExpectedResult: model.HitList{
{ID: 1, UID: "folder1", Title: "folder1", Type: model.DashHitFolder},
{ID: 2, UID: "folder2", Title: "folder2", Type: model.DashHitFolder},
{ID: 3, UID: "dash3", Title: "dash3", FolderUID: "folder2", Type: model.DashHitDB},
},
}
sc.acmock.GetUserPermissionsFunc = func(ctx context.Context, user *user.SignedInUser, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
return []accesscontrol.Permission{
{Action: "folders:read", Scope: "folders:*"},
{Action: "folders:write", Scope: "folders:uid:folder2"},
{Action: "dashboards:read", Scope: "dashboards:*"},
{Action: "dashboards:write", Scope: "folders:uid:folder2"},
}, nil
type testCase struct {
desc string
includeMetadata bool
permissions []accesscontrol.Permission
expectedMetadata map[int64]map[string]struct{}
}
type withMeta struct {
@ -42,37 +27,75 @@ func TestHTTPServer_Search(t *testing.T) {
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
}
t.Run("should attach access control metadata to response", func(t *testing.T) {
recorder := callAPI(sc.server, http.MethodGet, "/api/search?accesscontrol=true", nil, t)
assert.Equal(t, http.StatusOK, recorder.Code)
var result []withMeta
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
tests := []testCase{
{
desc: "should attach metadata to response",
includeMetadata: true,
expectedMetadata: map[int64]map[string]struct{}{
1: {dashboards.ActionFoldersRead: {}},
2: {dashboards.ActionFoldersRead: {}, dashboards.ActionFoldersWrite: {}, dashboards.ActionDashboardsWrite: {}},
3: {dashboards.ActionDashboardsRead: {}, dashboards.ActionDashboardsWrite: {}},
},
permissions: []accesscontrol.Permission{
{Action: "folders:read", Scope: "folders:*"},
{Action: "folders:write", Scope: "folders:uid:folder2"},
{Action: "dashboards:read", Scope: "dashboards:*"},
{Action: "dashboards:write", Scope: "folders:uid:folder2"},
},
},
{
desc: "not attach metadata",
includeMetadata: false,
expectedMetadata: map[int64]map[string]struct{}{},
permissions: []accesscontrol.Permission{
{Action: "folders:read", Scope: "folders:*"},
{Action: "folders:write", Scope: "folders:uid:folder2"},
{Action: "dashboards:read", Scope: "dashboards:*"},
{Action: "dashboards:write", Scope: "folders:uid:folder2"},
},
},
}
for _, r := range result {
if r.ID == 1 {
assert.Len(t, r.AccessControl, 1)
assert.True(t, r.AccessControl[dashboards.ActionFoldersRead])
} else if r.ID == 2 {
assert.Len(t, r.AccessControl, 3)
assert.True(t, r.AccessControl[dashboards.ActionFoldersRead])
assert.True(t, r.AccessControl[dashboards.ActionFoldersWrite])
assert.True(t, r.AccessControl[dashboards.ActionDashboardsWrite])
} else if r.ID == 3 {
assert.Len(t, r.AccessControl, 2)
assert.True(t, r.AccessControl[dashboards.ActionDashboardsRead])
assert.True(t, r.AccessControl[dashboards.ActionDashboardsWrite])
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.SearchService = &mockSearchService{ExpectedResult: model.HitList{
{ID: 1, UID: "folder1", Title: "folder1", Type: model.DashHitFolder},
{ID: 2, UID: "folder2", Title: "folder2", Type: model.DashHitFolder},
{ID: 3, UID: "dash3", Title: "dash3", FolderUID: "folder2", Type: model.DashHitDB},
}}
})
url := "/api/search"
if tt.includeMetadata {
url += "?accesscontrol=true"
}
}
})
t.Run("should not attach access control metadata to response", func(t *testing.T) {
recorder := callAPI(sc.server, http.MethodGet, "/api/search", nil, t)
assert.Equal(t, http.StatusOK, recorder.Code)
var result []withMeta
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
res, err := server.Send(
webtest.RequestWithSignedInUser(
server.NewGetRequest(url), userWithPermissions(1, tt.permissions),
),
)
require.NoError(t, err)
for _, r := range result {
assert.Len(t, r.AccessControl, 0)
}
})
var result []withMeta
require.NoError(t, json.NewDecoder(res.Body).Decode(&result))
for _, r := range result {
if !tt.includeMetadata {
assert.Nil(t, r.AccessControl)
continue
}
assert.Len(t, r.AccessControl, len(tt.expectedMetadata[r.ID]))
for action := range r.AccessControl {
_, ok := tt.expectedMetadata[r.ID][action]
assert.True(t, ok)
}
}
require.NoError(t, res.Body.Close())
})
}
}