mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanels: Add RBAC support (#73475)
This commit is contained in:
parent
d003ffe439
commit
a12cb8cbf3
@ -136,6 +136,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `dashgpt` | Enable AI powered features in dashboards |
|
||||
| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions |
|
||||
| `requestInstrumentationStatusSource` | Include a status source label for request metrics and logs |
|
||||
| `libraryPanelRBAC` | Enables RBAC support for library panels |
|
||||
| `wargamesTesting` | Placeholder feature flag for internal testing |
|
||||
| `alertingInsights` | Show the new alerting insights landing page |
|
||||
| `externalCorePlugins` | Allow core plugins to be loaded as external |
|
||||
|
@ -125,6 +125,7 @@ export interface FeatureToggles {
|
||||
newBrowseDashboards?: boolean;
|
||||
sseGroupByDatasource?: boolean;
|
||||
requestInstrumentationStatusSource?: boolean;
|
||||
libraryPanelRBAC?: boolean;
|
||||
lokiRunQueriesInParallel?: boolean;
|
||||
wargamesTesting?: boolean;
|
||||
alertingInsights?: boolean;
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||
@ -408,6 +410,76 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{"Admin"},
|
||||
}
|
||||
|
||||
libraryPanelsCreatorRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:library.panels:creator",
|
||||
DisplayName: "Library panel creator",
|
||||
Description: "Create library panel in general folder.",
|
||||
Group: "Library panels",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
},
|
||||
},
|
||||
Grants: []string{"Editor"},
|
||||
}
|
||||
|
||||
libraryPanelsReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:library.panels:reader",
|
||||
DisplayName: "Library panel reader",
|
||||
Description: "Read all library panels.",
|
||||
Group: "Library panels",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: libraryelements.ActionLibraryPanelsRead, Scope: libraryelements.ScopeLibraryPanelsAll},
|
||||
},
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
}
|
||||
|
||||
libraryPanelsGeneralReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:library.panels:general.reader",
|
||||
DisplayName: "Library panel general reader",
|
||||
Description: "Read all library panels in general folder.",
|
||||
Group: "Library panels",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: libraryelements.ActionLibraryPanelsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
},
|
||||
},
|
||||
Grants: []string{"Viewer"},
|
||||
}
|
||||
|
||||
libraryPanelsWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:library.panels:writer",
|
||||
DisplayName: "Library panel writer",
|
||||
Group: "Library panels",
|
||||
Description: "Create, read, write or delete all library panels and their permissions.",
|
||||
Permissions: ac.ConcatPermissions(libraryPanelsReaderRole.Role.Permissions, []ac.Permission{
|
||||
{Action: libraryelements.ActionLibraryPanelsWrite, Scope: libraryelements.ScopeLibraryPanelsAll},
|
||||
{Action: libraryelements.ActionLibraryPanelsDelete, Scope: libraryelements.ScopeLibraryPanelsAll},
|
||||
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: libraryelements.ScopeLibraryPanelsAll},
|
||||
}),
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
}
|
||||
|
||||
libraryPanelsGeneralWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:library.panels:general.writer",
|
||||
DisplayName: "Library panel general writer",
|
||||
Group: "Library panels",
|
||||
Description: "Create, read, write or delete all library panels and their permissions in the general folder.",
|
||||
Permissions: ac.ConcatPermissions(libraryPanelsGeneralReaderRole.Role.Permissions, []ac.Permission{
|
||||
{Action: libraryelements.ActionLibraryPanelsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
{Action: libraryelements.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
|
||||
}),
|
||||
},
|
||||
Grants: []string{"Editor"},
|
||||
}
|
||||
|
||||
publicDashboardsWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:dashboards.public:writer",
|
||||
@ -447,15 +519,18 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{"Admin"},
|
||||
}
|
||||
|
||||
return hs.accesscontrolService.DeclareFixedRoles(
|
||||
provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
|
||||
roles := []ac.RoleRegistration{provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
|
||||
datasourcesIdReaderRole, orgReaderRole, orgWriterRole,
|
||||
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole,
|
||||
annotationsReaderRole, dashboardAnnotationsWriterRole, annotationsWriterRole,
|
||||
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
|
||||
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole,
|
||||
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole,
|
||||
)
|
||||
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole}
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
|
||||
roles = append(roles, libraryPanelsCreatorRole, libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole)
|
||||
}
|
||||
|
||||
return hs.accesscontrolService.DeclareFixedRoles(roles...)
|
||||
}
|
||||
|
||||
// Metadata helpers
|
||||
|
@ -468,6 +468,12 @@ const (
|
||||
// Feature Management actions
|
||||
ActionFeatureManagementRead = "featuremgmt.read"
|
||||
ActionFeatureManagementWrite = "featuremgmt.write"
|
||||
|
||||
// Library Panel actions
|
||||
ActionLibraryPanelsCreate = "library.panels:create"
|
||||
ActionLibraryPanelsRead = "library.panels:read"
|
||||
ActionLibraryPanelsWrite = "library.panels:write"
|
||||
ActionLibraryPanelsDelete = "library.panels:delete"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/retriever"
|
||||
@ -191,7 +192,7 @@ type FolderPermissionsService struct {
|
||||
*resourcepermissions.Service
|
||||
}
|
||||
|
||||
var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead}
|
||||
var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead}
|
||||
var FolderEditActions = append(FolderViewActions, []string{
|
||||
dashboards.ActionFoldersWrite,
|
||||
dashboards.ActionFoldersDelete,
|
||||
@ -199,6 +200,9 @@ var FolderEditActions = append(FolderViewActions, []string{
|
||||
accesscontrol.ActionAlertingRuleCreate,
|
||||
accesscontrol.ActionAlertingRuleUpdate,
|
||||
accesscontrol.ActionAlertingRuleDelete,
|
||||
libraryelements.ActionLibraryPanelsCreate,
|
||||
libraryelements.ActionLibraryPanelsWrite,
|
||||
libraryelements.ActionLibraryPanelsDelete,
|
||||
}...)
|
||||
var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...)
|
||||
|
||||
|
@ -746,6 +746,14 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "libraryPanelRBAC",
|
||||
Description: "Enables RBAC support for library panels",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "lokiRunQueriesInParallel",
|
||||
Description: "Enables running Loki queries in parallel",
|
||||
|
@ -106,6 +106,7 @@ reportingRetries,preview,@grafana/sharing-squad,false,false,true,false
|
||||
newBrowseDashboards,GA,@grafana/grafana-frontend-platform,false,false,false,true
|
||||
sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false,false
|
||||
requestInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,false,false,false,false
|
||||
libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,false,true,false
|
||||
lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false,false
|
||||
wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false,false
|
||||
alertingInsights,experimental,@grafana/alerting-squad,false,false,false,true
|
||||
|
|
@ -435,6 +435,10 @@ const (
|
||||
// Include a status source label for request metrics and logs
|
||||
FlagRequestInstrumentationStatusSource = "requestInstrumentationStatusSource"
|
||||
|
||||
// FlagLibraryPanelRBAC
|
||||
// Enables RBAC support for library panels
|
||||
FlagLibraryPanelRBAC = "libraryPanelRBAC"
|
||||
|
||||
// FlagLokiRunQueriesInParallel
|
||||
// Enables running Loki queries in parallel
|
||||
FlagLokiRunQueriesInParallel = "lokiRunQueriesInParallel"
|
||||
|
@ -406,7 +406,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv)
|
||||
require.NoError(t, err)
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn)
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac)
|
||||
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -481,7 +481,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, ac, dashSrv)
|
||||
require.NoError(t, err)
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff)
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac)
|
||||
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -602,7 +602,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
CanEditValue: true,
|
||||
})
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag)
|
||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac)
|
||||
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
69
pkg/services/libraryelements/accesscontrol.go
Normal file
69
pkg/services/libraryelements/accesscontrol.go
Normal file
@ -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)
|
||||
|
@ -830,7 +830,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
features := featuremgmt.WithFeatures()
|
||||
folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features)
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures())
|
||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac)
|
||||
service := LibraryPanelService{
|
||||
Cfg: cfg,
|
||||
SQLStore: sqlStore,
|
||||
|
@ -555,6 +555,111 @@ func (m *managedFolderAlertActionsRepeatMigrator) Exec(sess *xorm.Session, mg *m
|
||||
return nil
|
||||
}
|
||||
|
||||
const managedFolderLibraryPanelActionsMigratorID = "managed folder permissions library panel actions migration"
|
||||
|
||||
func AddManagedFolderLibraryPanelActionsMigration(mg *migrator.Migrator) {
|
||||
mg.AddMigration(managedFolderLibraryPanelActionsMigratorID, &managedFolderLibraryPanelActionsMigrator{})
|
||||
}
|
||||
|
||||
type managedFolderLibraryPanelActionsMigrator struct {
|
||||
migrator.MigrationBase
|
||||
}
|
||||
|
||||
func (m *managedFolderLibraryPanelActionsMigrator) SQL(dialect migrator.Dialect) string {
|
||||
return CodeMigrationSQL
|
||||
}
|
||||
|
||||
// TODO: Refactor with alerts migration
|
||||
func (m *managedFolderLibraryPanelActionsMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||
var ids []any
|
||||
if err := sess.SQL("SELECT id FROM role WHERE name LIKE 'managed:%'").Find(&ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var permissions []ac.Permission
|
||||
if err := sess.SQL("SELECT role_id, action, scope FROM permission WHERE role_id IN(?"+strings.Repeat(" ,?", len(ids)-1)+") AND scope LIKE 'folders:%'", ids...).Find(&permissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapped := make(map[int64]map[string][]ac.Permission, len(ids)-1)
|
||||
for _, p := range permissions {
|
||||
if mapped[p.RoleID] == nil {
|
||||
mapped[p.RoleID] = make(map[string][]ac.Permission)
|
||||
}
|
||||
mapped[p.RoleID][p.Scope] = append(mapped[p.RoleID][p.Scope], p)
|
||||
}
|
||||
|
||||
var toAdd []ac.Permission
|
||||
now := time.Now()
|
||||
|
||||
for id, a := range mapped {
|
||||
for scope, p := range a {
|
||||
if hasFolderView(p) {
|
||||
if !hasAction(ac.ActionLibraryPanelsRead, p) {
|
||||
toAdd = append(toAdd, ac.Permission{
|
||||
RoleID: id,
|
||||
Updated: now,
|
||||
Created: now,
|
||||
Scope: scope,
|
||||
Action: ac.ActionLibraryPanelsRead,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if hasFolderAdmin(p) || hasFolderEdit(p) {
|
||||
if !hasAction(ac.ActionLibraryPanelsCreate, p) {
|
||||
toAdd = append(toAdd, ac.Permission{
|
||||
RoleID: id,
|
||||
Updated: now,
|
||||
Created: now,
|
||||
Scope: scope,
|
||||
Action: ac.ActionLibraryPanelsCreate,
|
||||
})
|
||||
}
|
||||
if !hasAction(ac.ActionLibraryPanelsDelete, p) {
|
||||
toAdd = append(toAdd, ac.Permission{
|
||||
RoleID: id,
|
||||
Updated: now,
|
||||
Created: now,
|
||||
Scope: scope,
|
||||
Action: ac.ActionLibraryPanelsDelete,
|
||||
})
|
||||
}
|
||||
if !hasAction(ac.ActionLibraryPanelsWrite, p) {
|
||||
toAdd = append(toAdd, ac.Permission{
|
||||
RoleID: id,
|
||||
Updated: now,
|
||||
Created: now,
|
||||
Scope: scope,
|
||||
Action: ac.ActionLibraryPanelsWrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdd) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := batch(len(toAdd), batchSize, func(start, end int) error {
|
||||
if _, err := sess.InsertMulti(toAdd[start:end]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasFolderAdmin(permissions []ac.Permission) bool {
|
||||
return hasActions(folderPermissionTranslation[dashboards.PERMISSION_ADMIN], permissions)
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
||||
accesscontrol.AddAdminOnlyMigration(mg)
|
||||
accesscontrol.AddSeedAssignmentMigrations(mg)
|
||||
accesscontrol.AddManagedFolderAlertActionsRepeatFixedMigration(mg)
|
||||
accesscontrol.AddManagedFolderLibraryPanelActionsMigration(mg)
|
||||
|
||||
AddExternalAlertmanagerToDatasourceMigration(mg)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user