From a12cb8cbf3a9b33841b2f2cb1522be11de78c86a Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 12 Oct 2023 00:30:50 +0100 Subject: [PATCH] LibraryPanels: Add RBAC support (#73475) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/api/accesscontrol.go | 83 +++++++++++++- pkg/services/accesscontrol/models.go | 6 + .../ossaccesscontrol/permissions_services.go | 6 +- pkg/services/featuremgmt/registry.go | 8 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/folder/folderimpl/folder_test.go | 6 +- pkg/services/libraryelements/accesscontrol.go | 69 ++++++++++++ pkg/services/libraryelements/api.go | 70 ++++++++++-- pkg/services/libraryelements/database.go | 27 +++-- .../libraryelements/libraryelements.go | 8 +- .../librarypanels/librarypanels_test.go | 2 +- .../accesscontrol/dashboard_permissions.go | 105 ++++++++++++++++++ .../sqlstore/migrations/migrations.go | 1 + 16 files changed, 370 insertions(+), 28 deletions(-) create mode 100644 pkg/services/libraryelements/accesscontrol.go diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 1e1d429e4bf..6b3c5206fea 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 35a753964bc..4ef472d4b50 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -125,6 +125,7 @@ export interface FeatureToggles { newBrowseDashboards?: boolean; sseGroupByDatasource?: boolean; requestInstrumentationStatusSource?: boolean; + libraryPanelRBAC?: boolean; lokiRunQueriesInParallel?: boolean; wargamesTesting?: boolean; alertingInsights?: boolean; diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index c2cbec08a44..e3d5265229a 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -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 diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index f09ef0b4fec..a5b0a6ed08b 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -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 ( diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 3b469539b71..35c4b9cd657 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -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}...) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 28354e32caf..fd84f4c3a0a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 651b7a48fa9..8aa3465a746 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 820b0db4bdf..c50cdb739b8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index f9d85b577e9..0d2e5eaa6db 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -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) diff --git a/pkg/services/libraryelements/accesscontrol.go b/pkg/services/libraryelements/accesscontrol.go new file mode 100644 index 00000000000..4760f9cf2ae --- /dev/null +++ b/pkg/services/libraryelements/accesscontrol.go @@ -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 + }) +} diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 4b09a49a773..5bb3a03612a 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -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 { diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 2932bfa3e17..73df537ec8b 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -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 } diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 65f60cbc7f2..864f951ac75 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -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) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index d64c2cd4d82..2b3bae99b2a 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -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, diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index 51099bb4e7d..083a6fe2196 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -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) } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 56df4323cc9..4272a5cafff 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -89,6 +89,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { accesscontrol.AddAdminOnlyMigration(mg) accesscontrol.AddSeedAssignmentMigrations(mg) accesscontrol.AddManagedFolderAlertActionsRepeatFixedMigration(mg) + accesscontrol.AddManagedFolderLibraryPanelActionsMigration(mg) AddExternalAlertmanagerToDatasourceMigration(mg)