Access control: FGAC for annotation updates (#46462)

* proposal

* PR feedback

* fix canSave bug

* update scope naming

* linting

* linting

Co-authored-by: Ezequiel Victorero <ezequiel.victorero@grafana.com>
This commit is contained in:
Ieva
2022-03-18 16:33:21 +00:00
committed by GitHub
parent 6eecd021a4
commit f2450575b3
8 changed files with 280 additions and 48 deletions

View File

@@ -276,6 +276,34 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(models.ROLE_VIEWER)}, Grants: []string{string(models.ROLE_VIEWER)},
} }
localAnnotationsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:annotations.local:writer",
DisplayName: "Local annotation writer",
Description: "Update annotations associated with dashboards.",
Group: "Annotations",
Version: 1,
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeLocal},
},
},
Grants: []string{string(models.ROLE_VIEWER)},
}
annotationsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:annotations:writer",
DisplayName: "Annotation writer",
Description: "Update all annotations.",
Group: "Annotations",
Version: 1,
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsAll},
},
},
Grants: []string{string(models.ROLE_EDITOR)},
}
dashboardsCreatorRole := ac.RoleRegistration{ dashboardsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{ Role: ac.RoleDTO{
Version: 1, Version: 1,
@@ -378,7 +406,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
return hs.AccessControl.DeclareFixedRoles( return hs.AccessControl.DeclareFixedRoles(
provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole, provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole,
datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole, datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole,
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole, orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole,
annotationsReaderRole, localAnnotationsWriterRole, annotationsWriterRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole, dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyWriterRole, foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyWriterRole,
) )

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@@ -9,13 +10,15 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models" "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/annotations"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
func GetAnnotations(c *models.ReqContext) response.Response { func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
query := &annotations.ItemQuery{ query := &annotations.ItemQuery{
From: c.QueryInt64("from"), From: c.QueryInt64("from"),
To: c.QueryInt64("to"), To: c.QueryInt64("to"),
@@ -54,12 +57,20 @@ func (e *CreateAnnotationError) Error() string {
return e.message return e.message
} }
func PostAnnotation(c *models.ReqContext) response.Response { func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostAnnotationsCmd{} cmd := dtos.PostAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
} }
if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave {
var canSave bool
var err error
if cmd.DashboardId != 0 {
canSave, err = canSaveLocalAnnotation(c, cmd.DashboardId)
} else {
canSave = canSaveGlobalAnnotation(c)
}
if err != nil || !canSave {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
@@ -105,7 +116,7 @@ func formatGraphiteAnnotation(what string, data string) string {
return text return text
} }
func PostGraphiteAnnotation(c *models.ReqContext) response.Response { func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostGraphiteAnnotationsCmd{} cmd := dtos.PostGraphiteAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -160,7 +171,7 @@ func PostGraphiteAnnotation(c *models.ReqContext) response.Response {
}) })
} }
func UpdateAnnotation(c *models.ReqContext) response.Response { func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.UpdateAnnotationsCmd{} cmd := dtos.UpdateAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -178,7 +189,15 @@ func UpdateAnnotation(c *models.ReqContext) response.Response {
return resp return resp
} }
if canSave, err := canSaveByDashboardID(c, annotation.DashboardId); err != nil || !canSave { canSave := true
if annotation.GetType() == annotations.Local {
canSave, err = canSaveLocalAnnotation(c, annotation.DashboardId)
} else {
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
canSave = canSaveGlobalAnnotation(c)
}
}
if err != nil || !canSave {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
@@ -199,7 +218,7 @@ func UpdateAnnotation(c *models.ReqContext) response.Response {
return response.Success("Annotation updated") return response.Success("Annotation updated")
} }
func PatchAnnotation(c *models.ReqContext) response.Response { func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PatchAnnotationsCmd{} cmd := dtos.PatchAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -216,7 +235,15 @@ func PatchAnnotation(c *models.ReqContext) response.Response {
return resp return resp
} }
if canSave, err := canSaveByDashboardID(c, annotation.DashboardId); err != nil || !canSave { canSave := true
if annotation.GetType() == annotations.Local {
canSave, err = canSaveLocalAnnotation(c, annotation.DashboardId)
} else {
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
canSave = canSaveGlobalAnnotation(c)
}
}
if err != nil || !canSave {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
@@ -253,7 +280,7 @@ func PatchAnnotation(c *models.ReqContext) response.Response {
return response.Success("Annotation patched") return response.Success("Annotation patched")
} }
func DeleteAnnotations(c *models.ReqContext) response.Response { func (hs *HTTPServer) DeleteAnnotations(c *models.ReqContext) response.Response {
cmd := dtos.DeleteAnnotationsCmd{} cmd := dtos.DeleteAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil { if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err) return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -274,7 +301,7 @@ func DeleteAnnotations(c *models.ReqContext) response.Response {
return response.Success("Annotations deleted") return response.Success("Annotations deleted")
} }
func DeleteAnnotationByID(c *models.ReqContext) response.Response { func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Response {
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64) annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
if err != nil { if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err) return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
@@ -287,7 +314,13 @@ func DeleteAnnotationByID(c *models.ReqContext) response.Response {
return resp return resp
} }
if canSave, err := canSaveByDashboardID(c, annotation.DashboardId); err != nil || !canSave { var canSave bool
if annotation.GetType() == annotations.Local {
canSave, err = canSaveLocalAnnotation(c, annotation.DashboardId)
} else {
canSave = canSaveGlobalAnnotation(c)
}
if err != nil || !canSave {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
@@ -302,21 +335,19 @@ func DeleteAnnotationByID(c *models.ReqContext) response.Response {
return response.Success("Annotation deleted") return response.Success("Annotation deleted")
} }
func canSaveByDashboardID(c *models.ReqContext, dashboardID int64) (bool, error) { func canSaveLocalAnnotation(c *models.ReqContext, dashboardID int64) (bool, error) {
if dashboardID == 0 && !c.SignedInUser.HasRole(models.ROLE_EDITOR) { guard := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
return false, nil if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
} return false, err
if dashboardID != 0 {
guard := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
} }
return true, nil return true, nil
} }
func canSaveGlobalAnnotation(c *models.ReqContext) bool {
return c.SignedInUser.HasRole(models.ROLE_EDITOR)
}
func findAnnotationByID(repo annotations.Repository, annotationID int64, orgID int64) (*annotations.ItemDTO, response.Response) { func findAnnotationByID(repo annotations.Repository, annotationID int64, orgID int64) (*annotations.ItemDTO, response.Response) {
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: orgID}) items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: orgID})
@@ -331,7 +362,7 @@ func findAnnotationByID(repo annotations.Repository, annotationID int64, orgID i
return items[0], nil return items[0], nil
} }
func GetAnnotationTags(c *models.ReqContext) response.Response { func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response {
query := &annotations.TagsQuery{ query := &annotations.TagsQuery{
OrgID: c.OrgId, OrgID: c.OrgId,
Tag: c.Query("tag"), Tag: c.Query("tag"),
@@ -346,3 +377,33 @@ func GetAnnotationTags(c *models.ReqContext) response.Response {
return response.JSON(200, annotations.GetAnnotationTagsResponse{Result: result}) return response.JSON(200, annotations.GetAnnotationTagsResponse{Result: result})
} }
// AnnotationTypeScopeResolver provides an AttributeScopeResolver able to
// resolve annotation types. Scope "annotations:id:<id>" will be translated to "annotations:type:<type>,
// where <type> is the type of annotation with id <id>.
func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveFunc) {
annotationTypeResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
scopeParts := strings.Split(initialScope, ":")
if scopeParts[0] != accesscontrol.ScopeAnnotationsRoot || len(scopeParts) != 3 {
return "", accesscontrol.ErrInvalidScope
}
annotationIdStr := scopeParts[2]
annotationId, err := strconv.Atoi(annotationIdStr)
if err != nil {
return "", accesscontrol.ErrInvalidScope
}
annotation, resp := findAnnotationByID(annotations.GetRepository(), int64(annotationId), orgID)
if resp != nil {
return "", err
}
if annotation.GetType() == annotations.Global {
return accesscontrol.ScopeAnnotationsTypeGlobal, nil
} else {
return accesscontrol.ScopeAnnotationsTypeLocal, nil
}
}
return accesscontrol.ScopeAnnotationsProvider.GetResourceScope(""), annotationTypeResolver
}

View File

@@ -7,6 +7,9 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
@@ -14,12 +17,16 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAnnotationsAPIEndpoint(t *testing.T) { 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) { t.Run("Given an annotation without a dashboard ID", func(t *testing.T) {
cmd := dtos.PostAnnotationsCmd{ cmd := dtos.PostAnnotationsCmd{
Time: 1000, Time: 1000,
@@ -65,7 +72,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
"/api/annotations/:annotationId", role, func(sc *scenarioContext) { "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo) annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, 403, sc.resp.Code)
}, mock) }, mock)
@@ -95,7 +102,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
"/api/annotations/:annotationId", role, func(sc *scenarioContext) { "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo) annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
}, mock) }, mock)
@@ -157,7 +164,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
setUpACL() setUpACL()
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo) annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, 403, sc.resp.Code)
}, mock) }, mock)
@@ -190,7 +197,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
setUpACL() setUpACL()
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo) annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
}, mock) }, mock)
@@ -230,6 +237,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
} }
type fakeAnnotationsRepo struct { type fakeAnnotationsRepo struct {
annotations map[int64]annotations.ItemDTO
} }
func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error { func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error {
@@ -243,6 +251,9 @@ func (repo *fakeAnnotationsRepo) Update(item *annotations.Item) error {
return nil return nil
} }
func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) { func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
if annotation, has := repo.annotations[query.AnnotationId]; has {
return []*annotations.ItemDTO{&annotation}, nil
}
annotations := []*annotations.ItemDTO{{Id: 1}} annotations := []*annotations.ItemDTO{{Id: 1}}
return annotations, nil return annotations, nil
} }
@@ -260,6 +271,11 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers) t.Cleanup(bus.ClearBusHandlers)
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd) c.Req.Body = mockRequestBody(cmd)
@@ -269,7 +285,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc.context.OrgId = testOrgID sc.context.OrgId = testOrgID
sc.context.OrgRole = role sc.context.OrgRole = role
return PostAnnotation(c) return hs.PostAnnotation(c)
}) })
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -286,6 +302,11 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers) t.Cleanup(bus.ClearBusHandlers)
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd) c.Req.Body = mockRequestBody(cmd)
@@ -295,7 +316,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
sc.context.OrgId = testOrgID sc.context.OrgId = testOrgID
sc.context.OrgRole = role sc.context.OrgRole = role
return UpdateAnnotation(c) return hs.UpdateAnnotation(c)
}) })
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -311,6 +332,11 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd) c.Req.Body = mockRequestBody(cmd)
@@ -320,7 +346,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc.context.OrgId = testOrgID sc.context.OrgId = testOrgID
sc.context.OrgRole = role sc.context.OrgRole = role
return PatchAnnotation(c) return hs.PatchAnnotation(c)
}) })
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -337,6 +363,11 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
hs.SQLStore = store
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd) c.Req.Body = mockRequestBody(cmd)
@@ -346,7 +377,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
sc.context.OrgId = testOrgID sc.context.OrgId = testOrgID
sc.context.OrgRole = role sc.context.OrgRole = role
return DeleteAnnotations(c) return hs.DeleteAnnotations(c)
}) })
fakeAnnoRepo = &fakeAnnotationsRepo{} fakeAnnoRepo = &fakeAnnotationsRepo{}
@@ -364,6 +395,21 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err) require.NoError(t, err)
repo := annotations.GetRepository()
localAnnotation := annotations.Item{
OrgId: sc.initCtx.OrgId,
DashboardId: 1,
}
globalAnnotation := annotations.Item{
OrgId: sc.initCtx.OrgId,
}
err = repo.Save(&localAnnotation)
require.NoError(t, err)
err = repo.Save(&globalAnnotation)
require.NoError(t, err)
type args struct { type args struct {
permissions []*accesscontrol.Permission permissions []*accesscontrol.Permission
url string url string
@@ -383,7 +429,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations", url: "/api/annotations",
method: http.MethodGet, method: http.MethodGet,
}, },
want: 200, want: http.StatusOK,
}, },
{ {
name: "AccessControl getting annotations without permissions is forbidden", name: "AccessControl getting annotations without permissions is forbidden",
@@ -392,7 +438,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations", url: "/api/annotations",
method: http.MethodGet, method: http.MethodGet,
}, },
want: 403, want: http.StatusForbidden,
}, },
{ {
name: "AccessControl getting tags for annotations with correct permissions is allowed", name: "AccessControl getting tags for annotations with correct permissions is allowed",
@@ -401,7 +447,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations/tags", url: "/api/annotations/tags",
method: http.MethodGet, method: http.MethodGet,
}, },
want: 200, want: http.StatusOK,
}, },
{ {
name: "AccessControl getting tags for annotations without correct permissions is forbidden", name: "AccessControl getting tags for annotations without correct permissions is forbidden",
@@ -410,7 +456,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations/tags", url: "/api/annotations/tags",
method: http.MethodGet, method: http.MethodGet,
}, },
want: 403, want: http.StatusForbidden,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -422,6 +468,66 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
} }
} }
func TestService_AnnotationTypeScopeResolver(t *testing.T) {
type testCaseResolver struct {
desc string
given string
want string
wantErr error
}
testCases := []testCaseResolver{
{
desc: "correctly resolves local annotations",
given: "annotations:id:1",
want: accesscontrol.ScopeAnnotationsTypeLocal,
wantErr: nil,
},
{
desc: "correctly resolves global annotations",
given: "annotations:id:2",
want: accesscontrol.ScopeAnnotationsTypeGlobal,
wantErr: nil,
},
{
desc: "invalid annotation ID",
given: "annotations:id:123abc",
want: "",
wantErr: accesscontrol.ErrInvalidScope,
},
{
desc: "malformed scope",
given: "annotations:1",
want: "",
wantErr: accesscontrol.ErrInvalidScope,
},
}
localAnnotation := annotations.ItemDTO{Id: 1, DashboardId: 1}
globalAnnotation := annotations.ItemDTO{Id: 2}
fakeAnnoRepo = &fakeAnnotationsRepo{
annotations: map[int64]annotations.ItemDTO{1: localAnnotation, 2: globalAnnotation},
}
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(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.Equal(t, tc.want, resolved)
}
})
}
}
func setUpACL() { func setUpACL() {
viewerRole := models.ROLE_VIEWER viewerRole := models.ROLE_VIEWER
editorRole := models.ROLE_EDITOR editorRole := models.ROLE_EDITOR

View File

@@ -436,16 +436,16 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup)) orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup))
}) })
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(GetAnnotations)) apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(hs.GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, routing.Wrap(DeleteAnnotations)) apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, routing.Wrap(hs.DeleteAnnotations))
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) { apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
annotationsRoute.Post("/", routing.Wrap(PostAnnotation)) annotationsRoute.Post("/", routing.Wrap(hs.PostAnnotation))
annotationsRoute.Delete("/:annotationId", routing.Wrap(DeleteAnnotationByID)) annotationsRoute.Delete("/:annotationId", routing.Wrap(hs.DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", routing.Wrap(UpdateAnnotation)) annotationsRoute.Put("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.UpdateAnnotation))
annotationsRoute.Patch("/:annotationId", routing.Wrap(PatchAnnotation)) annotationsRoute.Patch("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.PatchAnnotation))
annotationsRoute.Post("/graphite", reqEditorRole, routing.Wrap(PostGraphiteAnnotation)) annotationsRoute.Post("/graphite", reqEditorRole, routing.Wrap(hs.PostGraphiteAnnotation))
annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsTagsRead, ac.ScopeAnnotationsTagsAll)), routing.Wrap(GetAnnotationTags)) annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsTagsRead, ac.ScopeAnnotationsTagsAll)), routing.Wrap(hs.GetAnnotationTags))
}) })
apiRoute.Post("/frontend-metrics", routing.Wrap(hs.PostFrontendMetrics)) apiRoute.Post("/frontend-metrics", routing.Wrap(hs.PostFrontendMetrics))

View File

@@ -253,6 +253,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
} }
hs.registerRoutes() hs.registerRoutes()
// Register access control scope resolver for annotations
hs.AccessControl.RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
if err := hs.declareFixedRoles(); err != nil { if err := hs.declareFixedRoles(); err != nil {
return nil, err return nil, err
} }

View File

@@ -319,11 +319,9 @@ const (
// Annotations related actions // Annotations related actions
ActionAnnotationsRead = "annotations:read" ActionAnnotationsRead = "annotations:read"
ActionAnnotationsWrite = "annotations:write"
ActionAnnotationsTagsRead = "annotations.tags:read" ActionAnnotationsTagsRead = "annotations.tags:read"
ScopeAnnotationsAll = "annotations:*"
ScopeAnnotationsTagsAll = "annotations:tags:*"
// Dashboard actions // Dashboard actions
ActionDashboardsCreate = "dashboards:create" ActionDashboardsCreate = "dashboards:create"
ActionDashboardsRead = "dashboards:read" ActionDashboardsRead = "dashboards:read"
@@ -372,6 +370,17 @@ const (
var ( var (
// Team scope // Team scope
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId")) ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
// Annotation scopes
ScopeAnnotationsRoot = "annotations"
ScopeAnnotationsProvider = NewScopeProvider(ScopeAnnotationsRoot)
ScopeAnnotationsAll = ScopeAnnotationsProvider.GetResourceAllScope()
ScopeAnnotationsID = Scope(ScopeAnnotationsRoot, "id", Parameter(":annotationId"))
ScopeAnnotationsTypeLocal = ScopeAnnotationsProvider.GetResourceScopeType("dashboard")
ScopeAnnotationsTypeGlobal = ScopeAnnotationsProvider.GetResourceScopeType("organization")
// Annotation tag scopes
ScopeAnnotationsTagsAll = "annotations:tags:*"
) )
const RoleGrafanaAdmin = "Grafana Admin" const RoleGrafanaAdmin = "Grafana Admin"

View File

@@ -30,6 +30,10 @@ func GetResourceScopeName(resource string, resourceID string) string {
return Scope(resource, "name", resourceID) return Scope(resource, "name", resourceID)
} }
func GetResourceScopeType(resource string, typeName string) string {
return Scope(resource, "type", typeName)
}
func GetResourceAllScope(resource string) string { func GetResourceAllScope(resource string) string {
return Scope(resource, "*") return Scope(resource, "*")
} }
@@ -179,6 +183,7 @@ type ScopeProvider interface {
GetResourceScope(resourceID string) string GetResourceScope(resourceID string) string
GetResourceScopeUID(resourceID string) string GetResourceScopeUID(resourceID string) string
GetResourceScopeName(resourceID string) string GetResourceScopeName(resourceID string) string
GetResourceScopeType(typeName string) string
GetResourceAllScope() string GetResourceAllScope() string
GetResourceAllIDScope() string GetResourceAllIDScope() string
} }
@@ -209,6 +214,11 @@ func (s scopeProviderImpl) GetResourceScopeName(resourceID string) string {
return GetResourceScopeName(s.root, resourceID) return GetResourceScopeName(s.root, resourceID)
} }
// GetResourceScopeType returns scope that has the format "<rootScope>:type:<typeName>"
func (s scopeProviderImpl) GetResourceScopeType(typeName string) string {
return GetResourceScopeType(s.root, typeName)
}
// GetResourceAllScope returns scope that has the format "<rootScope>:*" // GetResourceAllScope returns scope that has the format "<rootScope>:*"
func (s scopeProviderImpl) GetResourceAllScope() string { func (s scopeProviderImpl) GetResourceAllScope() string {
return GetResourceAllScope(s.root) return GetResourceAllScope(s.root)

View File

@@ -145,3 +145,17 @@ type ItemDTO struct {
AvatarUrl string `json:"avatarUrl"` AvatarUrl string `json:"avatarUrl"`
Data *simplejson.Json `json:"data"` Data *simplejson.Json `json:"data"`
} }
type annotationType int
const (
Global annotationType = iota
Local
)
func (annotation *ItemDTO) GetType() annotationType {
if annotation.DashboardId != 0 {
return Local
}
return Global
}