diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 7555a413722..e6df844a3af 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -36,6 +37,11 @@ var ( // grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" // that HTTPServer needs func (hs *HTTPServer) declareFixedRoles() error { + // Declare plugins roles + if err := plugins.DeclareRBACRoles(hs.AccessControl); err != nil { + return err + } + provisioningWriterRole := ac.RoleRegistration{ Role: ac.RoleDTO{ Name: "fixed:provisioning:writer", diff --git a/pkg/api/api.go b/pkg/api/api.go index 60ea3ca91f5..b6385a34ecc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -88,8 +89,10 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/plugins/:id/", reqSignedIn, hs.Index) r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index) - r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page - r.Get("/a/:id", reqSignedIn, hs.Index) + // App Root Page + appPluginIDScope := plugins.ScopeProvider.GetResourceScope(":id") + r.Get("/a/:id/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index) + r.Get("/a/:id", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index) r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) @@ -325,12 +328,13 @@ func (hs *HTTPServer) registerRoutes() { datasourceRoute.Get("/id/:name", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionIDRead, nameScope)), routing.Wrap(hs.GetDataSourceIdByName)) }) + pluginIDScope := plugins.ScopeProvider.GetResourceScope(":pluginId") apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList)) - apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) + apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) // RBAC check performed in handler for App Plugins apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown)) apiRoute.Get("/plugins/:pluginId/health", routing.Wrap(hs.CheckHealth)) - apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource) - apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource) + apiRoute.Any("/plugins/:pluginId/resources", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource) + apiRoute.Any("/plugins/:pluginId/resources/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource) apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList)) if hs.Cfg.PluginAdminEnabled && !hs.Cfg.PluginAdminExternalManageEnabled { diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index c855edde892..03d30848d72 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -42,6 +43,11 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) { ReqSignedIn: true, })) + // Preventing access to plugin routes if the user has no right to access the plugin + authorize := ac.Middleware(hs.AccessControl) + handlers = append(handlers, authorize(middleware.ReqSignedIn, + ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID)))) + if route.ReqRole != "" { if route.ReqRole == models.ROLE_ADMIN { handlers = append(handlers, middleware.RoleAuth(models.ROLE_ADMIN)) @@ -49,6 +55,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) { handlers = append(handlers, middleware.RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN)) } } + handlers = append(handlers, AppPluginRoute(route, plugin.ID, hs)) for _, method := range strings.Split(route.Method, ",") { r.Handle(strings.TrimSpace(method), url, handlers) diff --git a/pkg/api/index.go b/pkg/api/index.go index 00c375931c6..9ab27c752c5 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -76,6 +76,7 @@ func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink { } func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) { + hasAccess := ac.HasAccess(hs.AccessControl, c) enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgId) if err != nil { return nil, err @@ -87,6 +88,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) continue } + if !hasAccess(ac.ReqSignedIn, + ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) { + continue + } + appLink := &dtos.NavLink{ Text: plugin.Name, Id: "plugin-page-" + plugin.ID, diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bed48fa2ebb..ffe3101a016 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -119,7 +119,17 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) if !exists { - return response.Error(404, "Plugin not found, no installed plugin with that id", nil) + return response.Error(http.StatusNotFound, "Plugin not found, no installed plugin with that id", nil) + } + + // In a first iteration, we only have one permission for app plugins. + // We will need a different permission to allow users to configure the plugin without needing access to it. + if plugin.IsApp() { + hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) + if !hasAccess(accesscontrol.ReqSignedIn, + accesscontrol.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) { + return response.Error(http.StatusForbidden, "Access Denied", nil) + } } dto := &dtos.PluginSetting{ diff --git a/pkg/plugins/accesscontrol.go b/pkg/plugins/accesscontrol.go new file mode 100644 index 00000000000..be402538dc8 --- /dev/null +++ b/pkg/plugins/accesscontrol.go @@ -0,0 +1,30 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +const ( + ActionAppAccess = "plugins.app:access" +) + +var ( + ScopeProvider = ac.NewScopeProvider("plugins") +) + +func DeclareRBACRoles(acService ac.AccessControl) error { + AppPluginsReader := ac.RoleRegistration{ + Role: ac.RoleDTO{ + Name: ac.FixedRolePrefix + "plugins.app:reader", + DisplayName: "Application Plugins Access", + Description: "Access application plugins (still enforcing the organization role)", + Group: "Plugins", + Permissions: []ac.Permission{ + {Action: ActionAppAccess, Scope: ScopeProvider.GetResourceAllScope()}, + }, + }, + Grants: []string{string(models.ROLE_VIEWER)}, + } + return acService.DeclareFixedRoles(AppPluginsReader) +} diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 308635bbb89..96c903a05a1 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -7,6 +7,11 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "gopkg.in/ini.v1" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -35,12 +40,6 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus" "github.com/grafana/grafana/pkg/tsdb/tempo" "github.com/grafana/grafana/pkg/tsdb/testdatasource" - "go.opentelemetry.io/otel/trace" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gopkg.in/ini.v1" ) func TestPluginManager_int_init(t *testing.T) { diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 22f1c3e233c..d29318cad33 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -7,12 +7,11 @@ import ( "testing" "time" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin"