diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 53d3b6e0912..ed5ffc53ef5 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -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, ) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 449e49aeccb..ee7577a8f66 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -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:" will be translated to "annotations:type:, +// where is the type of annotation with 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 +} diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index b1dba8d0aa7..1617994b356 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -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 diff --git a/pkg/api/api.go b/pkg/api/api.go index 9f69e6f417f..bbe16b0ad06 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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)) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 91aee6e9c2a..cac6ba7fcb9 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -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 } diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index ad6537bde7f..ac20cca91f7 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -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" diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index 326100d2d67..69f2533537f 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -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 ":type:" +func (s scopeProviderImpl) GetResourceScopeType(typeName string) string { + return GetResourceScopeType(s.root, typeName) +} + // GetResourceAllScope returns scope that has the format ":*" func (s scopeProviderImpl) GetResourceAllScope() string { return GetResourceAllScope(s.root) diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index cc7f4192409..8e9f2c2a538 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -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 +}