LibraryPanels: Add RBAC support (#73475)

This commit is contained in:
kay delaney 2023-10-12 00:30:50 +01:00 committed by GitHub
parent d003ffe439
commit a12cb8cbf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 370 additions and 28 deletions

View File

@ -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 |

View File

@ -125,6 +125,7 @@ export interface FeatureToggles {
newBrowseDashboards?: boolean;
sseGroupByDatasource?: boolean;
requestInstrumentationStatusSource?: boolean;
libraryPanelRBAC?: boolean;
lokiRunQueriesInParallel?: boolean;
wargamesTesting?: boolean;
alertingInsights?: boolean;

View File

@ -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

View File

@ -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 (

View File

@ -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}...)

View File

@ -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",

View File

@ -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

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
106 newBrowseDashboards GA @grafana/grafana-frontend-platform false false false true
107 sseGroupByDatasource experimental @grafana/observability-metrics false false false false
108 requestInstrumentationStatusSource experimental @grafana/plugins-platform-backend false false false false
109 libraryPanelRBAC experimental @grafana/dashboards-squad false false true false
110 lokiRunQueriesInParallel privatePreview @grafana/observability-logs false false false false
111 wargamesTesting experimental @grafana/hosted-grafana-team false false false false
112 alertingInsights experimental @grafana/alerting-squad false false false true

View File

@ -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"

View File

@ -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)

View 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
})
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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)
}

View File

@ -89,6 +89,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
accesscontrol.AddAdminOnlyMigration(mg)
accesscontrol.AddSeedAssignmentMigrations(mg)
accesscontrol.AddManagedFolderAlertActionsRepeatFixedMigration(mg)
accesscontrol.AddManagedFolderLibraryPanelActionsMigration(mg)
AddExternalAlertmanagerToDatasourceMigration(mg)