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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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)},
}
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{
Role: ac.RoleDTO{
Version: 1,
@ -378,7 +406,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
return hs.AccessControl.DeclareFixedRoles(
provisioningWriterRole, datasourcesReaderRole, datasourcesWriterRole, datasourcesIdReaderRole,
datasourcesCompatibilityReaderRole, orgReaderRole, orgWriterRole,
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole, annotationsReaderRole,
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole,
annotationsReaderRole, localAnnotationsWriterRole, annotationsWriterRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyWriterRole,
)

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"net/http"
"strconv"
@ -9,13 +10,15 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"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/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"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{
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
@ -54,12 +57,20 @@ func (e *CreateAnnotationError) Error() string {
return e.message
}
func PostAnnotation(c *models.ReqContext) response.Response {
func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
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)
}
@ -105,7 +116,7 @@ func formatGraphiteAnnotation(what string, data string) string {
return text
}
func PostGraphiteAnnotation(c *models.ReqContext) response.Response {
func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostGraphiteAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
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{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@ -178,7 +189,15 @@ func UpdateAnnotation(c *models.ReqContext) response.Response {
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)
}
@ -199,7 +218,7 @@ func UpdateAnnotation(c *models.ReqContext) response.Response {
return response.Success("Annotation updated")
}
func PatchAnnotation(c *models.ReqContext) response.Response {
func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PatchAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@ -216,7 +235,15 @@ func PatchAnnotation(c *models.ReqContext) response.Response {
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)
}
@ -253,7 +280,7 @@ func PatchAnnotation(c *models.ReqContext) response.Response {
return response.Success("Annotation patched")
}
func DeleteAnnotations(c *models.ReqContext) response.Response {
func (hs *HTTPServer) DeleteAnnotations(c *models.ReqContext) response.Response {
cmd := dtos.DeleteAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
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")
}
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)
if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
@ -287,7 +314,13 @@ func DeleteAnnotationByID(c *models.ReqContext) response.Response {
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)
}
@ -302,21 +335,19 @@ func DeleteAnnotationByID(c *models.ReqContext) response.Response {
return response.Success("Annotation deleted")
}
func canSaveByDashboardID(c *models.ReqContext, dashboardID int64) (bool, error) {
if dashboardID == 0 && !c.SignedInUser.HasRole(models.ROLE_EDITOR) {
return false, nil
}
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
}
func canSaveLocalAnnotation(c *models.ReqContext, dashboardID int64) (bool, error) {
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
}
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) {
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
}
func GetAnnotationTags(c *models.ReqContext) response.Response {
func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response {
query := &annotations.TagsQuery{
OrgID: c.OrgId,
Tag: c.Query("tag"),
@ -346,3 +377,33 @@ func GetAnnotationTags(c *models.ReqContext) response.Response {
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"
"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"
@ -14,12 +17,16 @@ import (
"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/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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,
@ -65,7 +72,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
}, mock)
@ -95,7 +102,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
"/api/annotations/:annotationId", role, func(sc *scenarioContext) {
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}, mock)
@ -157,7 +164,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
setUpACL()
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 403, sc.resp.Code)
}, mock)
@ -190,7 +197,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
setUpACL()
fakeAnnoRepo = &fakeAnnotationsRepo{}
annotations.SetRepository(fakeAnnoRepo)
sc.handlerFunc = DeleteAnnotationByID
sc.handlerFunc = hs.DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}, mock)
@ -230,6 +237,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) {
}
type fakeAnnotationsRepo struct {
annotations map[int64]annotations.ItemDTO
}
func (repo *fakeAnnotationsRepo) Delete(params *annotations.DeleteParams) error {
@ -243,6 +251,9 @@ func (repo *fakeAnnotationsRepo) Update(item *annotations.Item) error {
return nil
}
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}}
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.Cleanup(bus.ClearBusHandlers)
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)
@ -269,7 +285,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return PostAnnotation(c)
return hs.PostAnnotation(c)
})
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.Cleanup(bus.ClearBusHandlers)
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)
@ -295,7 +316,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return UpdateAnnotation(c)
return hs.UpdateAnnotation(c)
})
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) {
defer bus.ClearBusHandlers()
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)
@ -320,7 +346,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return PatchAnnotation(c)
return hs.PatchAnnotation(c)
})
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) {
defer bus.ClearBusHandlers()
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)
@ -346,7 +377,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
sc.context.OrgId = testOrgID
sc.context.OrgRole = role
return DeleteAnnotations(c)
return hs.DeleteAnnotations(c)
})
fakeAnnoRepo = &fakeAnnotationsRepo{}
@ -364,6 +395,21 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
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 {
permissions []*accesscontrol.Permission
url string
@ -383,7 +429,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations",
method: http.MethodGet,
},
want: 200,
want: http.StatusOK,
},
{
name: "AccessControl getting annotations without permissions is forbidden",
@ -392,7 +438,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
url: "/api/annotations",
method: http.MethodGet,
},
want: 403,
want: http.StatusForbidden,
},
{
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",
method: http.MethodGet,
},
want: 200,
want: http.StatusOK,
},
{
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",
method: http.MethodGet,
},
want: 403,
want: http.StatusForbidden,
},
}
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() {
viewerRole := models.ROLE_VIEWER
editorRole := models.ROLE_EDITOR

View File

@ -436,16 +436,16 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup))
})
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, routing.Wrap(DeleteAnnotations))
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(hs.GetAnnotations))
apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, routing.Wrap(hs.DeleteAnnotations))
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
annotationsRoute.Post("/", routing.Wrap(PostAnnotation))
annotationsRoute.Delete("/:annotationId", routing.Wrap(DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", routing.Wrap(UpdateAnnotation))
annotationsRoute.Patch("/:annotationId", routing.Wrap(PatchAnnotation))
annotationsRoute.Post("/graphite", reqEditorRole, routing.Wrap(PostGraphiteAnnotation))
annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsTagsRead, ac.ScopeAnnotationsTagsAll)), routing.Wrap(GetAnnotationTags))
annotationsRoute.Post("/", routing.Wrap(hs.PostAnnotation))
annotationsRoute.Delete("/:annotationId", routing.Wrap(hs.DeleteAnnotationByID))
annotationsRoute.Put("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.UpdateAnnotation))
annotationsRoute.Patch("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.PatchAnnotation))
annotationsRoute.Post("/graphite", reqEditorRole, routing.Wrap(hs.PostGraphiteAnnotation))
annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsTagsRead, ac.ScopeAnnotationsTagsAll)), routing.Wrap(hs.GetAnnotationTags))
})
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()
// Register access control scope resolver for annotations
hs.AccessControl.RegisterAttributeScopeResolver(AnnotationTypeScopeResolver())
if err := hs.declareFixedRoles(); err != nil {
return nil, err
}

View File

@ -319,11 +319,9 @@ const (
// Annotations related actions
ActionAnnotationsRead = "annotations:read"
ActionAnnotationsWrite = "annotations:write"
ActionAnnotationsTagsRead = "annotations.tags:read"
ScopeAnnotationsAll = "annotations:*"
ScopeAnnotationsTagsAll = "annotations:tags:*"
// Dashboard actions
ActionDashboardsCreate = "dashboards:create"
ActionDashboardsRead = "dashboards:read"
@ -372,6 +370,17 @@ const (
var (
// Team scope
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"

View File

@ -30,6 +30,10 @@ func GetResourceScopeName(resource string, resourceID string) string {
return Scope(resource, "name", resourceID)
}
func GetResourceScopeType(resource string, typeName string) string {
return Scope(resource, "type", typeName)
}
func GetResourceAllScope(resource string) string {
return Scope(resource, "*")
}
@ -179,6 +183,7 @@ type ScopeProvider interface {
GetResourceScope(resourceID string) string
GetResourceScopeUID(resourceID string) string
GetResourceScopeName(resourceID string) string
GetResourceScopeType(typeName string) string
GetResourceAllScope() string
GetResourceAllIDScope() string
}
@ -209,6 +214,11 @@ func (s scopeProviderImpl) GetResourceScopeName(resourceID string) string {
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>:*"
func (s scopeProviderImpl) GetResourceAllScope() string {
return GetResourceAllScope(s.root)

View File

@ -145,3 +145,17 @@ type ItemDTO struct {
AvatarUrl string `json:"avatarUrl"`
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
}