LibraryPanels: Add RBAC support (#73475)
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
)
|
||||
|
||||
const (
|
||||
ScopeLibraryPanelsRoot = "library.panels"
|
||||
ScopeLibraryPanelsPrefix = "library.panels:uid:"
|
||||
|
||||
ActionLibraryPanelsCreate = "library.panels:create"
|
||||
ActionLibraryPanelsRead = "library.panels:read"
|
||||
ActionLibraryPanelsWrite = "library.panels:write"
|
||||
ActionLibraryPanelsDelete = "library.panels:delete"
|
||||
)
|
||||
|
||||
var (
|
||||
ScopeLibraryPanelsProvider = ac.NewScopeProvider(ScopeLibraryPanelsRoot)
|
||||
|
||||
ScopeLibraryPanelsAll = ScopeLibraryPanelsProvider.GetResourceAllScope()
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoElementsFound = errors.New("library element not found")
|
||||
ErrElementNameNotUnique = errors.New("several library elements with the same name were found")
|
||||
)
|
||||
|
||||
// LibraryPanelUIDScopeResolver provides a ScopeAttributeResolver that is able to convert a scope prefixed with "library.panels:uid:"
|
||||
// into uid based scopes for a library panel and its associated folder hierarchy
|
||||
func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||
prefix := ScopeLibraryPanelsProvider.GetResourceScopeUID("")
|
||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||
if !strings.HasPrefix(scope, prefix) {
|
||||
return nil, ac.ErrInvalidScope
|
||||
}
|
||||
|
||||
uid, err := ac.ParseScopeUID(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := appcontext.User(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
libElDTO, err := l.getLibraryElementByUid(ctx, user, model.GetLibraryElementCommand{
|
||||
UID: uid,
|
||||
FolderName: dashboards.RootFolderName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, libElDTO.FolderUID, folderSvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(inheritedScopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(libElDTO.FolderUID), ScopeLibraryPanelsProvider.GetResourceScopeUID(uid)), nil
|
||||
})
|
||||
}
|
||||
@@ -6,23 +6,38 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func (l *LibraryElementService) registerAPIEndpoints() {
|
||||
authorize := ac.Middleware(l.AccessControl)
|
||||
|
||||
l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) {
|
||||
entities.Post("/", middleware.ReqSignedIn, routing.Wrap(l.createHandler))
|
||||
entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(l.deleteHandler))
|
||||
entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler))
|
||||
entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler))
|
||||
entities.Get("/:uid/connections/", middleware.ReqSignedIn, routing.Wrap(l.getConnectionsHandler))
|
||||
entities.Get("/name/:name", middleware.ReqSignedIn, routing.Wrap(l.getByNameHandler))
|
||||
entities.Patch("/:uid", middleware.ReqSignedIn, routing.Wrap(l.patchHandler))
|
||||
uidScope := ScopeLibraryPanelsProvider.GetResourceScopeUID(ac.Parameter(":uid"))
|
||||
|
||||
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
|
||||
entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler))
|
||||
entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler))
|
||||
entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler))
|
||||
entities.Get("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsRead, uidScope)), routing.Wrap(l.getHandler))
|
||||
entities.Get("/:uid/connections/", authorize(ac.EvalPermission(ActionLibraryPanelsRead, uidScope)), routing.Wrap(l.getConnectionsHandler))
|
||||
entities.Get("/name/:name", routing.Wrap(l.getByNameHandler))
|
||||
entities.Patch("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsWrite, uidScope)), routing.Wrap(l.patchHandler))
|
||||
} else {
|
||||
entities.Post("/", routing.Wrap(l.createHandler))
|
||||
entities.Delete("/:uid", routing.Wrap(l.deleteHandler))
|
||||
entities.Get("/", routing.Wrap(l.getAllHandler))
|
||||
entities.Get("/:uid", routing.Wrap(l.getHandler))
|
||||
entities.Get("/:uid/connections/", routing.Wrap(l.getConnectionsHandler))
|
||||
entities.Get("/name/:name", routing.Wrap(l.getByNameHandler))
|
||||
entities.Patch("/:uid", routing.Wrap(l.patchHandler))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,7 +71,8 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
|
||||
cmd.FolderID = folder.ID
|
||||
}
|
||||
}
|
||||
element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
|
||||
element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to create library element")
|
||||
}
|
||||
@@ -109,6 +125,7 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon
|
||||
// Responses:
|
||||
// 200: getLibraryElementResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -154,6 +171,14 @@ func (l *LibraryElementService) getAllHandler(c *contextmodel.ReqContext) respon
|
||||
return toLibraryElementError(err, "Failed to get library elements")
|
||||
}
|
||||
|
||||
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
|
||||
filteredPanels, err := l.filterLibraryPanelsByPermission(c, elementsResult.Elements)
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, "Failed to evaluate permissions")
|
||||
}
|
||||
elementsResult.Elements = filteredPanels
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, model.LibraryElementSearchResponse{Result: elementsResult})
|
||||
}
|
||||
|
||||
@@ -216,6 +241,7 @@ func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) respons
|
||||
// Responses:
|
||||
// 200: getLibraryElementConnectionsResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) getConnectionsHandler(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -244,7 +270,31 @@ func (l *LibraryElementService) getByNameHandler(c *contextmodel.ReqContext) res
|
||||
return toLibraryElementError(err, "Failed to get library element")
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: elements})
|
||||
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
|
||||
filteredElements, err := l.filterLibraryPanelsByPermission(c, elements)
|
||||
if err != nil {
|
||||
return toLibraryElementError(err, err.Error())
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: filteredElements})
|
||||
} else {
|
||||
return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: elements})
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) filterLibraryPanelsByPermission(c *contextmodel.ReqContext, elements []model.LibraryElementDTO) ([]model.LibraryElementDTO, error) {
|
||||
filteredPanels := make([]model.LibraryElementDTO, 0)
|
||||
for _, p := range elements {
|
||||
allowed, err := l.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ActionLibraryPanelsRead, ScopeLibraryPanelsProvider.GetResourceScopeUID(p.UID)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if allowed {
|
||||
filteredPanels = append(filteredPanels, p)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPanels, nil
|
||||
}
|
||||
|
||||
func toLibraryElementError(err error, message string) response.Response {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/kinds/librarypanel"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -82,7 +83,7 @@ func syncFieldsWithModel(libraryElement *model.LibraryElement) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLibraryElement(dialect migrator.Dialect, session *db.Session, uid string, orgID int64) (model.LibraryElementWithMeta, error) {
|
||||
func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string, orgID int64) (model.LibraryElementWithMeta, error) {
|
||||
elements := make([]model.LibraryElementWithMeta, 0)
|
||||
sql := selectLibraryElementDTOWithMeta +
|
||||
", coalesce(dashboard.title, 'General') AS folder_name" +
|
||||
@@ -161,8 +162,18 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
}
|
||||
|
||||
err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
|
||||
return err
|
||||
if l.features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
|
||||
allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(*cmd.FolderUID)))
|
||||
if !allowed {
|
||||
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", *cmd.FolderUID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := session.Insert(&element); err != nil {
|
||||
if l.SQLStore.GetDialect().IsUniqueConstraintViolation(err) {
|
||||
@@ -208,7 +219,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
var elementID int64
|
||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -520,7 +531,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU
|
||||
return model.LibraryElementDTO{}, err
|
||||
}
|
||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
elementInDB, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
elementInDB, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -537,7 +548,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU
|
||||
return model.ErrLibraryElementUIDTooLong
|
||||
}
|
||||
|
||||
_, err := getLibraryElement(l.SQLStore.GetDialect(), session, updateUID, signedInUser.GetOrgID())
|
||||
_, err := GetLibraryElement(l.SQLStore.GetDialect(), session, updateUID, signedInUser.GetOrgID())
|
||||
if !errors.Is(err, model.ErrLibraryElementNotFound) {
|
||||
return model.ErrLibraryElementAlreadyExists
|
||||
}
|
||||
@@ -634,7 +645,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser i
|
||||
}
|
||||
|
||||
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -737,7 +748,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context,
|
||||
return err
|
||||
}
|
||||
for _, elementUID := range elementUIDs {
|
||||
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, elementUID, signedInUser.GetOrgID())
|
||||
element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, elementUID, signedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles) *LibraryElementService {
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) *LibraryElementService {
|
||||
l := &LibraryElementService{
|
||||
Cfg: cfg,
|
||||
SQLStore: sqlStore,
|
||||
@@ -22,8 +23,12 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
|
||||
folderService: folderService,
|
||||
log: log.New("library-elements"),
|
||||
features: features,
|
||||
AccessControl: ac,
|
||||
}
|
||||
|
||||
l.registerAPIEndpoints()
|
||||
ac.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(l, l.folderService))
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
@@ -45,6 +50,7 @@ type LibraryElementService struct {
|
||||
folderService folder.Service
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
AccessControl accesscontrol.AccessControl
|
||||
}
|
||||
|
||||
var _ Service = (*LibraryElementService)(nil)
|
||||
|
||||
Reference in New Issue
Block a user