grafana/pkg/services/libraryelements/libraryelements_test.go
Dimitris Sotirakis 605d056136
Security: Sync security changes on main (#45083)
* * Teams: Appropriately apply user id filter in /api/teams/:id and /api/teams/search
* Teams: Ensure that users searching for teams are only able see teams they have access to
* Teams: Require teamGuardian admin privileges to list team members
* Teams: Prevent org viewers from administering teams
* Teams: Add org_id condition to team count query
* Teams: clarify permission requirements in teams api docs
* Teams: expand scenarios for team search tests
* Teams: mock teamGuardian in tests

Co-authored-by: Dan Cech <dcech@grafana.com>

* remove duplicate WHERE statement

* Fix for CVE-2022-21702

(cherry picked from commit 202d7c190082c094bc1dc13f7fe9464746c37f9e)

* Lint and test fixes

(cherry picked from commit 3e6b67d5504abf4a1d7b8d621f04d062c048e981)

* check content type properly

(cherry picked from commit 70b4458892bf2f776302720c10d24c9ff34edd98)

* basic csrf origin check

(cherry picked from commit 3adaa5ff39832364f6390881fb5b42ad47df92e1)

* compare origin to host

(cherry picked from commit 5443892699e8ed42836bb2b9a44744ff3e970f42)

* simplify url parsing

(cherry picked from commit b2ffbc9513fed75468628370a48b929d30af2b1d)

* check csrf for GET requests, only compare origin

(cherry picked from commit 8b81dc12d8f8a1f07852809c5b4d44f0f0b1d709)

* parse content type properly

(cherry picked from commit 16f76f4902e6f2188bea9606c68b551af186bdc0)

* mentioned get in the comment

(cherry picked from commit a7e61811ef8ae558ce721e2e3fed04ce7a5a5345)

* add content-type: application/json to test HTTP requests

* fix pluginproxy test

* Fix linter when comparing errors

Co-authored-by: Kevin Minehart <kmineh0151@gmail.com>
Co-authored-by: Dan Cech <dcech@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com>
Co-authored-by: Vardan Torosyan <vardants@gmail.com>
2022-02-09 13:44:38 +01:00

356 lines
10 KiB
Go

package libraryelements
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
dboards "github.com/grafana/grafana/pkg/dashboards"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
)
const userInDbName = "user_in_db"
const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad"
func TestDeleteLibraryPanelsInFolder(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := models.Dashboard{
Title: "Testing DeleteLibraryElementsInFolder",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid)
require.EqualError(t, err, ErrFolderHasConnectedLibraryElements.Error())
})
scenarioWithPanel(t, "When an admin tries to delete a folder uid that doesn't exist, it should fail",
func(t *testing.T, sc scenarioContext) {
err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid+"xxxx")
require.EqualError(t, err, models.ErrFolderNotFound.Error())
})
scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too",
func(t *testing.T, sc scenarioContext) {
command := getCreateVariableCommand(sc.folder.Id, "query0")
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
var result libraryElementsSearch
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 2, len(result.Result.Elements))
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.Uid)
require.NoError(t, err)
resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
err = json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
require.NotNil(t, result.Result)
require.Equal(t, 0, len(result.Result.Elements))
})
}
type libraryElement struct {
ID int64 `json:"id"`
OrgID int64 `json:"orgId"`
FolderID int64 `json:"folderId"`
UID string `json:"uid"`
Name string `json:"name"`
Kind int64 `json:"kind"`
Type string `json:"type"`
Description string `json:"description"`
Model map[string]interface{} `json:"model"`
Version int64 `json:"version"`
Meta LibraryElementDTOMeta `json:"meta"`
}
type libraryElementResult struct {
Result libraryElement `json:"result"`
}
type libraryElementArrayResult struct {
Result []libraryElement `json:"result"`
}
type libraryElementsSearch struct {
Result libraryElementsSearchResult `json:"result"`
}
type libraryElementsSearchResult struct {
TotalCount int64 `json:"totalCount"`
Elements []libraryElement `json:"elements"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, models.PanelElement, []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"id": 1,
"title": "Text - Library Panel",
"type": "text",
"description": "A description"
}
`))
return command
}
func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, models.VariableElement, []byte(`
{
"datasource": "${DS_GDEV-TESTDATA}",
"name": "query0",
"type": "query",
"description": "A description"
}
`))
return command
}
func getCreateCommandWithModel(folderID int64, name string, kind models.LibraryElementKind, model []byte) CreateLibraryElementCommand {
command := CreateLibraryElementCommand{
FolderID: folderID,
Name: name,
Model: model,
Kind: int64(kind),
}
return command
}
type scenarioContext struct {
ctx *web.Context
service *LibraryElementService
reqContext *models.ReqContext
user models.SignedInUser
folder *models.Folder
initialResult libraryElementResult
sqlStore *sqlstore.SQLStore
}
type folderACLItem struct {
roleType models.RoleType
permission models.PermissionType
}
func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard {
dash.FolderId = folderID
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: "",
OrgId: user.OrgId,
User: &user,
Overwrite: false,
}
origUpdateAlerting := dashboards.UpdateAlerting
t.Cleanup(func() {
dashboards.UpdateAlerting = origUpdateAlerting
})
dashboards.UpdateAlerting = func(ctx context.Context, store dboards.Store, orgID int64, dashboard *models.Dashboard,
user *models.SignedInUser) error {
return nil
}
dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(context.Background(), dashItem, true)
require.NoError(t, err)
return dashboard
}
func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string, user models.SignedInUser,
items []folderACLItem) *models.Folder {
t.Helper()
s := dashboards.NewFolderService(user.OrgId, &user, sqlStore)
t.Logf("Creating folder with title and UID %q", title)
folder, err := s.CreateFolder(context.Background(), title, title)
require.NoError(t, err)
updateFolderACL(t, sqlStore, folder.Id, items)
return folder
}
func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) {
t.Helper()
if len(items) == 0 {
return
}
var aclItems []*models.DashboardAcl
for _, item := range items {
role := item.roleType
permission := item.permission
aclItems = append(aclItems, &models.DashboardAcl{
DashboardID: folderID,
Role: &role,
Permission: permission,
Created: time.Now(),
Updated: time.Now(),
})
}
err := sqlStore.UpdateDashboardACL(context.Background(), folderID, aclItems)
require.NoError(t, err)
}
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = libraryElementResult{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}
func validateAndUnMarshalArrayResponse(t *testing.T, resp response.Response) libraryElementArrayResult {
t.Helper()
require.Equal(t, 200, resp.Status())
var result = libraryElementArrayResult{}
err := json.Unmarshal(resp.Body(), &result)
require.NoError(t, err)
return result
}
func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
testScenario(t, desc, func(t *testing.T, sc scenarioContext) {
command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel")
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
sc.initialResult = validateAndUnMarshalResponse(t, resp)
fn(t, sc)
})
}
// testScenario is a wrapper around t.Run performing common setup for library panel tests.
// It takes your real test function as a callback.
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
t.Run(desc, func(t *testing.T) {
ctx := web.Context{Req: &http.Request{
Header: http.Header{
"Content-Type": []string{"application/json"},
},
}}
orgID := int64(1)
role := models.ROLE_ADMIN
sqlStore := sqlstore.InitTestDB(t)
service := LibraryElementService{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
}
user := models.SignedInUser{
UserId: 1,
Name: "Signed In User",
Login: "signed_in_user",
Email: "signed.in.user@test.com",
OrgId: orgID,
OrgRole: role,
LastSeenAt: time.Now(),
}
// deliberate difference between signed in user and user in db to make it crystal clear
// what to expect in the tests
// In the real world these are identical
cmd := models.CreateUserCommand{
Email: "user.in.db@test.com",
Name: "User In DB",
Login: userInDbName,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
require.NoError(t, err)
sc := scenarioContext{
user: user,
ctx: &ctx,
service: &service,
sqlStore: sqlStore,
reqContext: &models.ReqContext{
Context: &ctx,
SignedInUser: &user,
},
}
sc.folder = createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{})
fn(t, sc)
})
}
func getCompareOptions() []cmp.Option {
return []cmp.Option{
cmp.Transformer("Time", func(in time.Time) int64 {
return in.UTC().Unix()
}),
}
}
func mockRequestBody(v interface{}) io.ReadCloser {
b, _ := json.Marshal(v)
return io.NopCloser(bytes.NewReader(b))
}