mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
6eecd021a4
commit
f2450575b3
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user