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

1004 lines
32 KiB

package api
import (
"context"
"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/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
)
func TestAnnotationsAPIEndpoint(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
t.Run("Given an annotation without a dashboard ID", func(t *testing.T) {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
patchCmd := dtos.PatchAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
t.Run("When user is an Org Viewer", func(t *testing.T) {
role := models.ROLE_VIEWER
t.Run("Should not be allowed to save an annotation", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role,
cmd, store, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId",
role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1",
"/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
}, mock)
})
})
t.Run("When user is an Org Editor", func(t *testing.T) {
role := models.ROLE_EDITOR
t.Run("Should be able to save an annotation", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role,
cmd, store, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}, mock)
})
})
})
t.Run("Given an annotation with a dashboard ID and the dashboard does not have an ACL", func(t *testing.T) {
cmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
DashboardId: 1,
PanelId: 1,
}
dashboardUIDCmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
DashboardUID: "home",
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
Id: 1,
}
patchCmd := dtos.PatchAnnotationsCmd{
Time: 8000,
Text: "annotation text 50",
Tags: []string{"foo", "bar"},
Id: 1,
}
deleteCmd := dtos.MassDeleteAnnotationsCmd{
DashboardId: 1,
PanelId: 1,
}
deleteWithDashboardUIDCmd := dtos.MassDeleteAnnotationsCmd{
DashboardUID: "home",
PanelId: 1,
}
t.Run("When user is an Org Viewer", func(t *testing.T) {
role := models.ROLE_VIEWER
t.Run("Should not be allowed to save an annotation", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
setUpACL()
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
}, mock)
})
})
t.Run("When user is an Org Editor", func(t *testing.T) {
role := models.ROLE_EDITOR
t.Run("Should be able to save an annotation", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
mock := mockstore.NewSQLStoreMock()
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1",
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
setUpACL()
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}, mock)
})
})
t.Run("When user is an Admin", func(t *testing.T) {
role := models.ROLE_ADMIN
mock := mockstore.NewSQLStoreMock()
mock.ExpectedDashboard = &models.Dashboard{
Id: 1,
Uid: "home",
}
t.Run("Should be able to do anything", func(t *testing.T) {
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, dashboardUIDCmd, mock, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
deleteAnnotationsScenario(t, "When calling POST on", "/api/annotations/mass-delete",
"/api/annotations/mass-delete", role, deleteCmd, store, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete",
"/api/annotations/mass-delete", role, deleteWithDashboardUIDCmd, mock, func(sc *scenarioContext) {
setUpACL()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
})
})
})
})
}
type fakeAnnotationsRepo struct {
annotations map[int64]annotations.Item
}
func NewFakeAnnotationsRepo() *fakeAnnotationsRepo {
return &fakeAnnotationsRepo{
annotations: map[int64]annotations.Item{},
}
}
func (repo *fakeAnnotationsRepo) Delete(_ context.Context, params *annotations.DeleteParams) error {
if params.Id != 0 {
delete(repo.annotations, params.Id)
} else {
for _, v := range repo.annotations {
if params.DashboardId == v.DashboardId && params.PanelId == v.PanelId {
delete(repo.annotations, v.Id)
}
}
}
return nil
}
func (repo *fakeAnnotationsRepo) Save(item *annotations.Item) error {
if item.Id == 0 {
item.Id = int64(len(repo.annotations) + 1)
}
repo.annotations[item.Id] = *item
return nil
}
func (repo *fakeAnnotationsRepo) Update(_ context.Context, item *annotations.Item) error {
return nil
}
func (repo *fakeAnnotationsRepo) Find(_ context.Context, query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
if annotation, has := repo.annotations[query.AnnotationId]; has {
return []*annotations.ItemDTO{{Id: annotation.Id, DashboardId: annotation.DashboardId}}, nil
}
annotations := []*annotations.ItemDTO{{Id: 1, DashboardId: 0}}
return annotations, nil
}
func (repo *fakeAnnotationsRepo) FindTags(_ context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) {
result := annotations.FindTagsResult{
Tags: []*annotations.TagsDTO{},
}
return result, nil
}
func (repo *fakeAnnotationsRepo) LoadItems() {
}
var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.PostAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = store
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
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>
3 years ago
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return hs.PostAnnotation(c)
})
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
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>
3 years ago
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return hs.UpdateAnnotation(c)
})
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}
func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
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>
3 years ago
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return hs.PatchAnnotation(c)
})
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.m.Patch(routePattern, sc.defaultHandler)
fn(sc)
})
}
func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.MassDeleteAnnotationsCmd, store sqlstore.Store, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = store
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
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>
3 years ago
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return hs.MassDeleteAnnotations(c)
})
fakeAnnoRepo = NewFakeAnnotationsRepo()
annotations.SetRepository(fakeAnnoRepo)
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func TestAPI_Annotations_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true, true)
setInitCtxSignedInEditor(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1}
organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0}
fakeAnnoRepo = NewFakeAnnotationsRepo()
_ = fakeAnnoRepo.Save(dashboardAnnotation)
_ = fakeAnnoRepo.Save(organizationAnnotation)
annotations.SetRepository(fakeAnnoRepo)
postOrganizationCmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
PanelId: 1,
}
postDashboardCmd := dtos.PostAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
DashboardId: 1,
PanelId: 1,
}
updateCmd := dtos.UpdateAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
patchCmd := dtos.PatchAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
postGraphiteCmd := dtos.PostGraphiteAnnotationsCmd{
When: 1000,
What: "annotation text",
Data: "Deploy",
Tags: []string{"tag1", "tag2"},
}
type args struct {
permissions []*accesscontrol.Permission
url string
body io.Reader
method string
}
tests := []struct {
name string
args args
want int
}{
{
name: "AccessControl getting annotations with correct permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}},
url: "/api/annotations",
method: http.MethodGet,
},
want: http.StatusOK,
},
{
name: "AccessControl getting annotations without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations",
method: http.MethodGet,
},
want: http.StatusForbidden,
},
{
name: "AccessControl getting annotation by ID with correct permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}},
url: "/api/annotations/1",
method: http.MethodGet,
},
want: http.StatusOK,
},
{
name: "AccessControl getting annotation by ID without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations",
method: http.MethodGet,
},
want: http.StatusForbidden,
},
{
name: "AccessControl getting tags for annotations with correct permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead}},
url: "/api/annotations/tags",
method: http.MethodGet,
},
want: http.StatusOK,
},
{
name: "AccessControl getting tags for annotations without correct permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite}},
url: "/api/annotations/tags",
method: http.MethodGet,
},
want: http.StatusForbidden,
},
{
name: "AccessControl update dashboard annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/1",
method: http.MethodPut,
body: mockRequestBody(updateCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl update dashboard annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations/1",
method: http.MethodPut,
body: mockRequestBody(updateCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl update organization annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsAll,
}},
url: "/api/annotations/2",
method: http.MethodPut,
body: mockRequestBody(updateCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl update organization annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/2",
method: http.MethodPut,
body: mockRequestBody(updateCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl patch dashboard annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/1",
method: http.MethodPatch,
body: mockRequestBody(patchCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl patch dashboard annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations/1",
method: http.MethodPatch,
body: mockRequestBody(patchCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl patch organization annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsAll,
}},
url: "/api/annotations/2",
method: http.MethodPatch,
body: mockRequestBody(patchCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl patch organization annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/2",
method: http.MethodPatch,
body: mockRequestBody(patchCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl create dashboard annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations",
method: http.MethodPost,
body: mockRequestBody(postDashboardCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl create dashboard annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations",
method: http.MethodPost,
body: mockRequestBody(postDashboardCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl create dashboard annotation with incorrect permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization,
}},
url: "/api/annotations",
method: http.MethodPost,
body: mockRequestBody(postDashboardCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl create organization annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsAll,
}},
url: "/api/annotations",
method: http.MethodPost,
body: mockRequestBody(postOrganizationCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl create organization annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations",
method: http.MethodPost,
body: mockRequestBody(postOrganizationCmd),
},
want: http.StatusForbidden,
},
{
name: "AccessControl delete dashboard annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/1",
method: http.MethodDelete,
},
want: http.StatusOK,
},
{
name: "AccessControl delete dashboard annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{},
url: "/api/annotations/1",
method: http.MethodDelete,
},
want: http.StatusForbidden,
},
{
name: "AccessControl delete organization annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsAll,
}},
url: "/api/annotations/2",
method: http.MethodDelete,
},
want: http.StatusOK,
},
{
name: "AccessControl delete organization annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/2",
method: http.MethodDelete,
},
want: http.StatusForbidden,
},
{
name: "AccessControl create graphite annotation with permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsAll,
}},
url: "/api/annotations/graphite",
method: http.MethodPost,
body: mockRequestBody(postGraphiteCmd),
},
want: http.StatusOK,
},
{
name: "AccessControl create organization annotation without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard,
}},
url: "/api/annotations/graphite",
method: http.MethodPost,
body: mockRequestBody(postGraphiteCmd),
},
want: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setUpRBACGuardian(t)
sc.acmock.
RegisterScopeAttributeResolver(AnnotationTypeScopeResolver())
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t)
assert.Equalf(t, tt.want, r.Code, "Annotations API(%v)", tt.args.url)
})
}
}
func TestService_AnnotationTypeScopeResolver(t *testing.T) {
type testCaseResolver struct {
desc string
given string
want string
wantErr error
}
testCases := []testCaseResolver{
{
desc: "correctly resolves dashboard annotations",
given: "annotations:id:1",
want: accesscontrol.ScopeAnnotationsTypeDashboard,
wantErr: nil,
},
{
desc: "correctly resolves organization annotations",
given: "annotations:id:2",
want: accesscontrol.ScopeAnnotationsTypeOrganization,
wantErr: nil,
},
{
desc: "invalid annotation ID",
given: "annotations:id:123abc",
want: "",
wantErr: accesscontrol.ErrInvalidScope,
},
{
desc: "malformed scope",
given: "annotations:1",
want: "",
wantErr: accesscontrol.ErrInvalidScope,
},
}
dashboardAnnotation := annotations.Item{Id: 1, DashboardId: 1}
organizationAnnotation := annotations.Item{Id: 2}
fakeAnnoRepo = NewFakeAnnotationsRepo()
_ = fakeAnnoRepo.Save(&dashboardAnnotation)
_ = fakeAnnoRepo.Save(&organizationAnnotation)
annotations.SetRepository(fakeAnnoRepo)
prefix, resolver := AnnotationTypeScopeResolver()
require.Equal(t, "annotations:id:", prefix)
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resolved, err := resolver.Resolve(context.Background(), 1, tc.given)
if tc.wantErr != nil {
require.Error(t, err)
require.Equal(t, tc.wantErr, err)
} else {
require.NoError(t, err)
require.Len(t, resolved, 1)
require.Equal(t, tc.want, resolved[0])
}
})
}
}
func TestAPI_MassDeleteAnnotations_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true, true)
setInitCtxSignedInEditor(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
type args struct {
permissions []*accesscontrol.Permission
url string
body io.Reader
method string
}
tests := []struct {
name string
args args
want int
}{
{
name: "Mass delete dashboard annotations without dashboardId is not allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
DashboardId: 0,
PanelId: 1,
}),
},
want: http.StatusBadRequest,
},
{
name: "Mass delete dashboard annotations without panelId is not allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
DashboardId: 10,
PanelId: 0,
}),
},
want: http.StatusBadRequest,
},
{
name: "AccessControl mass delete dashboard annotations with correct dashboardId and panelId as input is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
DashboardId: 1,
PanelId: 1,
}),
},
want: http.StatusOK,
},
{
name: "Mass delete organization annotations without input to delete all organization annotations is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
DashboardId: 0,
PanelId: 0,
}),
},
want: http.StatusOK,
},
{
name: "Mass delete organization annotations without permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
DashboardId: 0,
PanelId: 0,
}),
},
want: http.StatusForbidden,
},
{
name: "AccessControl mass delete dashboard annotations with correct annotationId as input is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
AnnotationId: 1,
}),
},
want: http.StatusOK,
},
{
name: "AccessControl mass delete annotation without access to dashboard annotations is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
AnnotationId: 1,
}),
},
want: http.StatusForbidden,
},
{
name: "AccessControl mass delete annotation without access to organization annotations is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
url: "/api/annotations/mass-delete",
method: http.MethodPost,
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{
AnnotationId: 2,
}),
},
want: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setUpRBACGuardian(t)
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgId)
dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1}
organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0}
fakeAnnoRepo = NewFakeAnnotationsRepo()
_ = fakeAnnoRepo.Save(dashboardAnnotation)
_ = fakeAnnoRepo.Save(organizationAnnotation)
annotations.SetRepository(fakeAnnoRepo)
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t)
assert.Equalf(t, tt.want, r.Code, "Annotations API(%v)", tt.args.url)
})
}
}
func setUpACL() {
viewerRole := models.ROLE_VIEWER
editorRole := models.ROLE_EDITOR
aclMockResp := []*models.DashboardAclInfoDTO{
{Role: &viewerRole, Permission: models.PERMISSION_VIEW},
{Role: &editorRole, Permission: models.PERMISSION_EDIT},
}
store := mockstore.NewSQLStoreMock()
store.ExpectedDashboardAclInfoList = aclMockResp
store.ExpectedTeamsByUser = []*models.TeamDTO{}
guardian.InitLegacyGuardian(store)
}
func setUpRBACGuardian(t *testing.T) {
origNewGuardian := guardian.New
t.Cleanup(func() {
guardian.New = origNewGuardian
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true})
}