The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/libraryelements/libraryelements_permissions...

372 lines
15 KiB

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
}