Files
grafana/pkg/services/libraryelements/libraryelements_permissions_test.go

373 lines
15 KiB
Go

package libraryelements_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"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/supportbundles/supportbundlestest"
"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/tests/testinfra"
)
func TestIntegrationLibraryElementPermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
quotaService := quotaimpl.ProvideService(env.SQLStore, env.Cfg)
orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotaService)
require.NoError(t, err)
sharedOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"})
require.NoError(t, err)
createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "viewer",
Login: "viewer",
OrgID: sharedOrg.ID,
})
createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
OrgID: sharedOrg.ID,
})
createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin2",
OrgID: sharedOrg.ID,
})
uid := ""
t.Run("create", func(t *testing.T) {
t.Run("When viewer tries to create a library panel in the General folder, it should fail", func(t *testing.T) {
createLibraryElement(t, grafanaListedAddr, "viewer", "viewer", "", http.StatusForbidden)
})
t.Run("When a user tries to create a library panel in a folder that doesn't exist, it should fail", func(t *testing.T) {
createLibraryElement(t, grafanaListedAddr, "admin2", "admin", "non-existent-folder-uid", http.StatusBadRequest)
})
t.Run("When editor tries to create a library panel in the General folder, it should succeed", func(t *testing.T) {
uid = createLibraryElement(t, grafanaListedAddr, "editor", "editor", "", http.StatusOK)
require.NotEmpty(t, uid)
require.NotEqual(t, uid, "")
})
})
t.Run("move to folder", func(t *testing.T) {
folderUID := createTestFolder(t, grafanaListedAddr)
t.Run("When viewer tries to move library panel to folder, it should fail", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, folderUID, http.StatusForbidden)
})
t.Run("When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "admin2", "admin", uid, "non-existent-folder-uid", http.StatusBadRequest)
})
t.Run("When editor tries to move library panel to folder, it should succeed", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, folderUID, http.StatusOK)
})
})
t.Run("move to general folder", func(t *testing.T) {
t.Run("When viewer tries to move library panel back to general, it should fail", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, "", http.StatusForbidden)
})
t.Run("When editor tries to move library panel back to general, it should succeed", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, "", http.StatusOK)
})
})
t.Run("get", func(t *testing.T) {
t.Run("When viewer tries to get library panel, it should succeed", func(t *testing.T) {
getLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, http.StatusOK)
})
t.Run("When editor tries to get library panel, it should succeed", func(t *testing.T) {
getLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, http.StatusOK)
})
})
t.Run("get all", func(t *testing.T) {
t.Run("When viewer tries to get all library elements, it should succeed", func(t *testing.T) {
getAllLibraryElements(t, grafanaListedAddr, "viewer", "viewer", http.StatusOK, 1)
})
t.Run("When editor tries to get all library elements, it should succeed", func(t *testing.T) {
getAllLibraryElements(t, grafanaListedAddr, "editor", "editor", http.StatusOK, 1)
})
})
t.Run("delete", func(t *testing.T) {
t.Run("When viewer tries to delete library panel, it should fail", func(t *testing.T) {
deleteLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, http.StatusForbidden)
})
t.Run("When editor tries to delete library panel, it should succeed", func(t *testing.T) {
deleteLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, http.StatusOK)
})
})
}
func TestIntegrationLibraryElementGranularPermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
quotaService := quotaimpl.ProvideService(env.SQLStore, env.Cfg)
orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotaService)
require.NoError(t, err)
sharedOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"})
require.NoError(t, err)
userID := createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "granular-viewer",
Login: "granular-viewer",
OrgID: sharedOrg.ID,
})
createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin2",
OrgID: sharedOrg.ID,
})
folder1UID := createTestFolder(t, grafanaListedAddr)
folder2UID := createTestFolder(t, grafanaListedAddr)
folder3UID := createTestFolder(t, grafanaListedAddr)
// viewer only has access to folder 1 & 3
grantFolderPermissions(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder1UID, userID)
grantFolderPermissions(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder3UID, userID)
// revoke view access to folder2
revokeFolderPermissions(t, grafanaListedAddr, folder2UID, userID)
uid := ""
t.Run("granular createpermissions", func(t *testing.T) {
t.Run("When viewer has write access to folder1, they can create library element in folder1", func(t *testing.T) {
uid = createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder1UID, http.StatusOK)
require.NotEmpty(t, uid)
})
t.Run("When viewer doesn't have read access to folder2, they cannot create library element in folder2", func(t *testing.T) {
createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder2UID, http.StatusBadRequest)
})
t.Run("When viewer doesn't have write access to general folder, they cannot create library element in general", func(t *testing.T) {
createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", "", http.StatusForbidden)
})
})
t.Run("granular move permissions", func(t *testing.T) {
t.Run("When viewer has write access to folder3 and folder1, they can move library element from folder1 to folder3", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, folder3UID, http.StatusOK)
})
t.Run("When viewer doesn't have read access to folder2, they cannot move library element to folder2", func(t *testing.T) {
patchLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, folder2UID, http.StatusBadRequest)
})
})
inGeneralFolder := createLibraryElement(t, grafanaListedAddr, "admin2", "admin", "", http.StatusOK)
inFolder2 := createLibraryElement(t, grafanaListedAddr, "admin2", "admin", folder2UID, http.StatusOK)
t.Run("granular read permissions", func(t *testing.T) {
t.Run("When viewer has read access to folder1, they can get library element from folder1", func(t *testing.T) {
getLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, http.StatusOK)
})
t.Run("When viewer doesn't have read access to folder2, they cannot get library element from folder2", func(t *testing.T) {
getLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inFolder2, http.StatusNotFound)
})
t.Run("When viewer has limited folder access, they only see library elements from accessible folders", func(t *testing.T) {
getAllLibraryElements(t, grafanaListedAddr, "granular-viewer", "granular-viewer", http.StatusOK, 2)
})
})
t.Run("granular delete permissions", func(t *testing.T) {
t.Run("When viewer has write access to folder1, they can delete library element from folder1", func(t *testing.T) {
deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, http.StatusOK)
})
t.Run("When viewer doesn't have write access to folder2, they cannot delete library element from folder2", func(t *testing.T) {
deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inFolder2, http.StatusForbidden)
})
t.Run("When viewer doesn't have write access to general folder, they cannot delete library element from general", func(t *testing.T) {
deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inGeneralFolder, http.StatusForbidden)
})
})
}
/*
Helper functions
*/
func createLibraryElement(t *testing.T, grafanaListedAddr, user, password, folderUID string, expectedStatus int) string {
m := map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description",
}
createRequest := map[string]interface{}{
"name": "Library Panel Name",
"model": m,
"folderUid": folderUID,
"kind": int64(1),
}
resp := makeHTTPRequest(t, "POST", fmt.Sprintf("http://%s:%s@%s/api/library-elements", user, password, grafanaListedAddr), createRequest, expectedStatus)
if expectedStatus == http.StatusOK {
var result model.LibraryElementResponse
err := json.Unmarshal(resp, &result)
require.NoError(t, err)
return result.Result.UID
}
return ""
}
func patchLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid, folderUID string, expectedStatus int) {
version := getLibraryElementVersion(t, grafanaListedAddr, user, password, uid)
patchRequest := map[string]interface{}{
"folderUid": folderUID,
"version": version,
"kind": 1,
}
makeHTTPRequest(t, "PATCH", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), patchRequest, expectedStatus)
}
func deleteLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid string, expectedStatus int) {
makeHTTPRequest(t, "DELETE", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, expectedStatus)
}
func getLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid string, expectedStatus int) {
makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, expectedStatus)
}
func getAllLibraryElements(t *testing.T, grafanaListedAddr, user, password string, expectedStatus int, expectedLength int) {
resp := makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements", user, password, grafanaListedAddr), nil, expectedStatus)
if expectedStatus == http.StatusOK {
var result model.LibraryElementSearchResponse
err := json.Unmarshal(resp, &result)
require.NoError(t, err)
require.Len(t, result.Result.Elements, expectedLength)
}
}
func getLibraryElementVersion(t *testing.T, grafanaListedAddr, user, password, uid string) int {
resp := makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, http.StatusOK)
var getResult model.LibraryElementResponse
err := json.Unmarshal(resp, &getResult)
require.NoError(t, err)
return int(getResult.Result.Version)
}
func createTestFolder(t *testing.T, grafanaListedAddr string) string {
folderRequest := map[string]interface{}{
"title": "Test Folder",
}
resp := makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders", grafanaListedAddr), folderRequest, http.StatusOK)
var folder models.Folder
err := json.Unmarshal(resp, &folder)
require.NoError(t, err)
return folder.UID
}
func grantFolderPermissions(t *testing.T, grafanaListedAddr, user, password, folderUID string, userID int64) {
permissionRequest := map[string]interface{}{
"items": []map[string]interface{}{
{
"userId": userID,
"permission": 2, // edit permission
},
},
}
makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID), permissionRequest, http.StatusOK)
}
func revokeFolderPermissions(t *testing.T, grafanaListedAddr, folderUID string, userID int64) {
permissionRequest := map[string]interface{}{
"items": []map[string]interface{}{},
}
makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID), permissionRequest, http.StatusOK)
}
func makeHTTPRequest(t *testing.T, method, url string, body interface{}, expectedStatus int) []byte {
var req *http.Request
var err error
if body != nil {
buf := &bytes.Buffer{}
err = json.NewEncoder(buf).Encode(body)
require.NoError(t, err)
req, err = http.NewRequest(method, url, buf)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
} else {
req, err = http.NewRequest(method, url, nil)
require.NoError(t, err)
}
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
// nolint:errcheck
defer resp.Body.Close()
require.Equal(t, expectedStatus, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
return respBody
}
func createUserInOrg(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 {
t.Helper()
cfg.AutoAssignOrg = true
cfg.AutoAssignOrgId = 1
quotaService := quotaimpl.ProvideService(db, cfg)
orgService, err := orgimpl.ProvideService(db, cfg, quotaService)
require.NoError(t, err)
usrSvc, err := userimpl.ProvideService(
db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(),
quotaService, supportbundlestest.NewFakeBundleService(),
)
require.NoError(t, err)
u, err := usrSvc.Create(context.Background(), &cmd)
require.NoError(t, err)
return u.ID
}