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/tests/api/annotations/annotations_test.go

437 lines
14 KiB

package annotations
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/services/dashboards"
"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/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationAnnotations(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableAuthZClientCache: true,
DisableAnonymous: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
noneUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleNone),
Login: "noneuser",
Password: "noneuser",
IsAdmin: false,
OrgID: 1,
})
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Login: "editor",
Password: "editor",
IsAdmin: false,
OrgID: 1,
})
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Login: "viewer",
Password: "viewer",
IsAdmin: false,
OrgID: 1,
})
savedFolder := createFolder(t, grafanaListedAddr, "Test Folder")
dash1 := createDashboard(t, grafanaListedAddr, "Dashboard 1", savedFolder.ID, savedFolder.UID) // nolint:staticcheck
dash2 := createDashboard(t, grafanaListedAddr, "Dashboard 2", savedFolder.ID, savedFolder.UID) // nolint:staticcheck
createAnnotation(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"dashboardId": dash1.ID,
"panelId": 1,
"text": "Dashboard 1 annotation",
"time": 1234567890000,
})
createAnnotation(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"dashboardId": dash2.ID,
"panelId": 1,
"text": "Dashboard 2 annotation",
"time": 1234567890000,
})
createAnnotation(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"text": "Organization annotation",
"time": 1234567890000,
})
t.Run("basic tests", func(t *testing.T) {
t.Run("should allow accessing annotations for specific dashboard", func(t *testing.T) {
url := fmt.Sprintf("http://admin:admin@%s/api/annotations?dashboardId=%d", grafanaListedAddr, dash1.ID)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 1)
})
t.Run("should allow accessing annotations for specific dashboard by UID", func(t *testing.T) {
url := fmt.Sprintf("http://admin:admin@%s/api/annotations?dashboardUID=%s", grafanaListedAddr, dash1.UID)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 1)
})
})
t.Run("access control tests", func(t *testing.T) {
viewPermissions := []map[string]interface{}{
{
"permission": 1,
"userId": noneUserID,
},
}
t.Run("should have no dashboards if missing annotation read permission on dashboards", func(t *testing.T) {
url := fmt.Sprintf("http://noneuser:noneuser@%s/api/annotations", grafanaListedAddr)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusForbidden)
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("should be able to see annotations for dashboards that user has access to", func(t *testing.T) {
setDashboardPermissions(t, grafanaListedAddr, dash1.UID, viewPermissions)
// should be able to get first one
url := fmt.Sprintf("http://noneuser:noneuser@%s/api/annotations?dashboardId=%d", grafanaListedAddr, dash1.ID)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
// cannot get the second one
url = fmt.Sprintf("http://noneuser:noneuser@%s/api/annotations?dashboardId=%d", grafanaListedAddr, dash2.ID)
resp, err = http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("should inherit folder permissions", func(t *testing.T) {
setFolderPermissions(t, grafanaListedAddr, savedFolder.UID, viewPermissions)
url := fmt.Sprintf("http://noneuser:noneuser@%s/api/annotations", grafanaListedAddr)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 2)
})
t.Run("should allow admin to access all annotations", func(t *testing.T) {
url := fmt.Sprintf("http://admin:admin@%s/api/annotations", grafanaListedAddr)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 3)
})
dash3 := createDashboard(t, grafanaListedAddr, "Dashboard 3", 0, "")
createAnnotation(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{
"dashboardId": dash3.ID,
"panelId": 1,
"text": "Dashboard 3 annotation",
"time": 1234567890000,
})
t.Run("should allow editor to access org annotations and annotations for dashboards they have access to (dash3)", func(t *testing.T) {
url := fmt.Sprintf("http://editor:editor@%s/api/annotations", grafanaListedAddr)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 2)
})
t.Run("should allow viewer to access org annotations and annotations for dashboards they have access to (dash3)", func(t *testing.T) {
url := fmt.Sprintf("http://viewer:viewer@%s/api/annotations", grafanaListedAddr)
resp, err := http.Get(url) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
var annotations []interface{}
err = json.Unmarshal(body, &annotations)
require.NoError(t, err)
assert.Len(t, annotations, 2)
})
t.Run("should allow editor to create org annotations", func(t *testing.T) {
annotationPayload := map[string]interface{}{
"text": "Test annotations",
"time": 1234567890000,
}
payloadBytes, err := json.Marshal(annotationPayload)
require.NoError(t, err)
url := fmt.Sprintf("http://editor:editor@%s/api/annotations", grafanaListedAddr)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("should deny viewer from creating org annotations", func(t *testing.T) {
annotationPayload := map[string]interface{}{
"text": "Test annotation",
"time": 1234567890000,
}
payloadBytes, err := json.Marshal(annotationPayload)
require.NoError(t, err)
url := fmt.Sprintf("http://viewer:viewer@%s/api/annotations", grafanaListedAddr)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("should allow editor to create dashboard annotations", func(t *testing.T) {
annotationPayload := map[string]interface{}{
"dashboardId": dash3.ID,
"panelId": 1,
"text": "Test annotations",
"time": 1234567890000,
}
payloadBytes, err := json.Marshal(annotationPayload)
require.NoError(t, err)
url := fmt.Sprintf("http://editor:editor@%s/api/annotations", grafanaListedAddr)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
})
t.Run("should deny viewer from creating dashboard annotations", func(t *testing.T) {
annotationPayload := map[string]interface{}{
"dashboardId": dash3.ID,
"panelId": 1,
"text": "Test annotation",
"time": 1234567890000,
}
payloadBytes, err := json.Marshal(annotationPayload)
require.NoError(t, err)
url := fmt.Sprintf("http://viewer:viewer@%s/api/annotations", grafanaListedAddr)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
})
})
}
func createAnnotation(t *testing.T, grafanaListedAddr string, username, password string, payload map[string]interface{}) {
t.Helper()
payloadBytes, err := json.Marshal(payload)
require.NoError(t, err)
url := fmt.Sprintf("http://%s:%s@%s/api/annotations", username, password, grafanaListedAddr)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func setDashboardPermissions(t *testing.T, grafanaListedAddr string, dashboardUID string, permissions []map[string]interface{}) {
t.Helper()
payload := map[string]interface{}{
"items": permissions,
}
payloadBytes, err := json.Marshal(payload)
require.NoError(t, err)
url := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s/permissions", grafanaListedAddr, dashboardUID)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payloadBytes))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func setFolderPermissions(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) {
t.Helper()
payload := map[string]interface{}{
"items": permissions,
}
payloadBytes, err := json.Marshal(payload)
require.NoError(t, err)
url := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payloadBytes))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
require.NoError(t, err)
}
func createFolder(t *testing.T, grafanaListedAddr string, title string) *dtos.Folder {
t.Helper()
buf1 := &bytes.Buffer{}
err := json.NewEncoder(buf1).Encode(folder.CreateFolderCommand{
Title: title,
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/folders", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", buf1)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var f *dtos.Folder
err = json.Unmarshal(b, &f)
require.NoError(t, err)
return f
}
func createDashboard(t *testing.T, grafanaListedAddr string, title string, folderID int64, folderUID string) *dashboards.Dashboard {
t.Helper()
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(map[string]interface{}{
"dashboard": map[string]interface{}{
"title": title,
},
"folderId": folderID,
"folderUid": folderUID,
"overwrite": true,
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", buf)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var saveResp struct {
Status string `json:"status"`
Slug string `json:"slug"`
Version int64 `json:"version"`
ID int64 `json:"id"`
UID string `json:"uid"`
URL string `json:"url"`
FolderUID string `json:"folderUid"`
}
err = json.Unmarshal(b, &saveResp)
require.NoError(t, err)
require.NotEmpty(t, saveResp.UID)
return &dashboards.Dashboard{
ID: saveResp.ID, // nolint:staticcheck
UID: saveResp.UID,
Slug: saveResp.Slug,
Version: int(saveResp.Version),
FolderUID: saveResp.FolderUID,
}
}