VYPR
Medium severity5.4NVD Advisory· Published Aug 20, 2024· Updated Apr 15, 2026

CVE-2024-6322

CVE-2024-6322

Description

Access control for plugin data sources protected by the ReqActions json field of the plugin.json is bypassed if the user or service account is granted associated access to any other data source, as the ReqActions check was not scoped to each specific datasource. The account must have prior query access to the impacted datasource.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/grafana/grafanaGo
>= 11.1.0, < 11.1.111.1.1
github.com/grafana/grafanaGo
>= 11.1.2, < 11.1.311.1.3
github.com/grafana/grafanaGo
>= 0.0.0-20240521130516-0072e4a92d89, < 0.0.0-20240725142242-c326d865c58b0.0.0-20240725142242-c326d865c58b
github.com/grafana/grafanaGo
>= 1.9.2-0.20240521130516-0072e4a92d89, < 1.9.2-0.20240725142242-c326d865c58b1.9.2-0.20240725142242-c326d865c58b

Patches

2
4cb3ba5d1a7a

RBAC: Allow plugins to use scoped actions (#90945)

https://github.com/grafana/grafanaKevin MinehartJul 25, 2024via ghsa
8 files changed · +246 86
  • pkg/api/pluginproxy/ds_proxy.go+5 5 modified
    @@ -19,11 +19,11 @@ import (
     	glog "github.com/grafana/grafana/pkg/infra/log"
     	"github.com/grafana/grafana/pkg/infra/tracing"
     	"github.com/grafana/grafana/pkg/plugins"
    -	"github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
     	"github.com/grafana/grafana/pkg/services/datasources"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
     	"github.com/grafana/grafana/pkg/services/oauthtoken"
    +	pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
     	"github.com/grafana/grafana/pkg/setting"
     	"github.com/grafana/grafana/pkg/util"
     	"github.com/grafana/grafana/pkg/util/proxyutil"
    @@ -341,12 +341,12 @@ func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool {
     	ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
     	useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
     	if useRBAC {
    -		routeEval := accesscontrol.EvalPermission(route.ReqAction)
    -		ok := routeEval.Evaluate(proxy.ctx.GetPermissions())
    -		if !ok {
    +		routeEval := pluginac.GetDataSourceRouteEvaluator(proxy.ds.UID, route.ReqAction)
    +		hasAccess := routeEval.Evaluate(proxy.ctx.GetPermissions())
    +		if !hasAccess {
     			ctxLogger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path, "action", route.ReqAction, "path", route.Path, "method", route.Method)
     		}
    -		return ok
    +		return hasAccess
     	}
     	if route.ReqRole.IsValid() {
     		if hasUserRole := proxy.ctx.HasUserRole(route.ReqRole); !hasUserRole {
    
  • pkg/api/pluginproxy/ds_proxy_test.go+55 1 modified
    @@ -108,9 +108,18 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
     				Path: "mypath",
     				URL:  "https://example.com/api/v1/",
     			},
    +			{
    +				Path:      "api/rbac-home",
    +				ReqAction: "datasources:read",
    +			},
    +			{
    +				Path:      "api/rbac-restricted",
    +				ReqAction: "test-app.settings:read",
    +			},
     		}
     
     		ds := &datasources.DataSource{
    +			UID: "dsUID",
     			JsonData: simplejson.NewFromAny(map[string]any{
     				"clientId":   "asd",
     				"dynamicUrl": "https://dynamic.grafana.com",
    @@ -249,6 +258,51 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
     				require.NoError(t, err)
     			})
     		})
    +
    +		t.Run("plugin route with RBAC protection user is allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app.settings:read": nil}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.NoError(t, err)
    +		})
    +
    +		t.Run("plugin route with RBAC protection user is not allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app:read": nil}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.Error(t, err)
    +		})
    +
    +		t.Run("plugin route with dynamic RBAC protection user is allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:dsUID"}}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.NoError(t, err)
    +		})
    +
    +		t.Run("plugin route with dynamic RBAC protection user is not allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			// Has access but to another app
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:notTheDsUID"}}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.Error(t, err)
    +		})
     	})
     
     	t.Run("Plugin with multiple routes for token auth", func(t *testing.T) {
    @@ -1021,7 +1075,7 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
     	cfg := setting.NewCfg()
     	secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
     	secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
    -	features := featuremgmt.WithFeatures()
    +	features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
     	dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
     		&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
     		plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
    
  • pkg/api/pluginproxy/pluginproxy.go+3 1 modified
    @@ -15,6 +15,7 @@ import (
     	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
     	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
     	"github.com/grafana/grafana/pkg/services/secrets"
     	"github.com/grafana/grafana/pkg/setting"
    @@ -130,7 +131,8 @@ func (proxy *PluginProxy) HandleRequest() {
     func (proxy *PluginProxy) hasAccessToRoute(route *plugins.Route) bool {
     	useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
     	if useRBAC {
    -		hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(ac.EvalPermission(route.ReqAction))
    +		routeEval := pluginac.GetPluginRouteEvaluator(proxy.ps.PluginID, route.ReqAction)
    +		hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(routeEval)
     		if !hasAccess {
     			proxy.ctx.Logger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path)
     		}
    
  • pkg/api/pluginproxy/pluginproxy_test.go+21 2 modified
    @@ -454,7 +454,13 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     			Path:      "projects",
     			Method:    "GET",
     			URL:       "http://localhost/api/projects",
    -			ReqAction: "plugin-id.projects:read", // Protected by RBAC action
    +			ReqAction: "test-app.projects:read", // Protected by RBAC action
    +		},
    +		{
    +			Path:      "home",
    +			Method:    "GET",
    +			URL:       "http://localhost/api/home",
    +			ReqAction: "plugins.app:access", // Protected by RBAC action with plugin scope
     		},
     	}
     
    @@ -479,7 +485,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     		},
     		{
     			proxyPath:       "/projects",
    -			usrPerms:        map[string][]string{"plugin-id.projects:read": {}},
    +			usrPerms:        map[string][]string{"test-app.projects:read": {}},
     			expectedURLPath: "/api/projects",
     			expectedStatus:  http.StatusOK,
     		},
    @@ -489,6 +495,18 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     			expectedURLPath: "/api/projects",
     			expectedStatus:  http.StatusForbidden,
     		},
    +		{
    +			proxyPath:       "/home",
    +			usrPerms:        map[string][]string{"plugins.app:access": {"plugins:id:not-the-test-app"}},
    +			expectedURLPath: "/api/home",
    +			expectedStatus:  http.StatusForbidden,
    +		},
    +		{
    +			proxyPath:       "/home",
    +			usrPerms:        map[string][]string{"plugins.app:access": {"plugins:id:test-app"}},
    +			expectedURLPath: "/api/home",
    +			expectedStatus:  http.StatusOK,
    +		},
     	}
     
     	for _, tc := range tcs {
    @@ -533,6 +551,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     				},
     			}
     			ps := &pluginsettings.DTO{
    +				PluginID:       "test-app",
     				SecureJSONData: map[string][]byte{},
     			}
     			cfg := &setting.Cfg{}
    
  • pkg/middleware/auth.go+1 1 modified
    @@ -127,7 +127,7 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, fea
     
     			if normalizeIncludePath(u.Path) == path {
     				useRBAC := features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && i.RequiresRBACAction()
    -				if useRBAC && !hasAccess(ac.EvalPermission(i.Action)) {
    +				if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) {
     					logger.Debug("Plugin include is covered by RBAC, user doesn't have access", "plugin", pluginID, "include", i.Name)
     					permitted = false
     					break
    
  • pkg/services/navtree/navtreeimpl/applinks.go+1 1 modified
    @@ -269,7 +269,7 @@ func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID st
     	hasAccess := ac.HasAccess(s.accessControl, c)
     	return func(include *plugins.Includes) bool {
     		useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
    -		if useRBAC && !hasAccess(ac.EvalPermission(include.Action)) {
    +		if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) {
     			s.log.Debug("plugin include is covered by RBAC, user doesn't have access",
     				"plugin", pluginID,
     				"include", include.Name)
    
  • pkg/services/navtree/navtreeimpl/applinks_test.go+120 75 modified
    @@ -406,20 +406,28 @@ func TestAddAppLinksAccessControl(t *testing.T) {
     			ID: "test-app1", Name: "Test app1 name", Type: plugins.TypeApp,
     			Includes: []*plugins.Includes{
     				{
    -					Name:       "Catalog",
    -					Path:       "/a/test-app1/catalog",
    +					Name:       "Home",
    +					Path:       "/a/test-app1/home",
     					Type:       "page",
     					AddToNav:   true,
     					DefaultNav: true,
    -					Role:       roletype.RoleEditor,
    -					Action:     catalogReadAction,
    +					Role:       roletype.RoleViewer,
     				},
     				{
    -					Name:     "Page2",
    -					Path:     "/a/test-app1/page2",
    +					Name:     "Catalog",
    +					Path:     "/a/test-app1/catalog",
    +					Type:     "page",
    +					AddToNav: true,
    +					Role:     roletype.RoleEditor,
    +					Action:   catalogReadAction,
    +				},
    +				{
    +					Name:     "Announcements",
    +					Path:     "/a/test-app1/announcements",
     					Type:     "page",
     					AddToNav: true,
     					Role:     roletype.RoleViewer,
    +					Action:   pluginaccesscontrol.ActionAppAccess,
     				},
     			},
     		},
    @@ -442,77 +450,114 @@ func TestAddAppLinksAccessControl(t *testing.T) {
     		},
     	}
     
    -	t.Run("Should not add app links when the user cannot access app plugins", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{}
    -		user.OrgRole = roletype.RoleAdmin
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		require.Len(t, treeRoot.Children, 0)
    -	})
    -	t.Run("Should add both includes when the user is an editor", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleEditor
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Url)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    -	})
    -	t.Run("Should add one include when the user is a viewer", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleViewer
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    -	})
    -	t.Run("Should add both includes when the user is a viewer with catalog read", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}, catalogReadAction: []string{}},
    -		}
    -		user.OrgRole = roletype.RoleViewer
    -		service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +	t.Run("Without plugin RBAC - Enforce role", func(t *testing.T) {
    +		t.Run("Should not add app links when the user cannot access app plugins", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{}
    +			user.OrgRole = roletype.RoleAdmin
     
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Url)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			require.Len(t, treeRoot.Children, 0)
    +		})
    +		t.Run(" Should add all includes when the user is an editor", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleEditor
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 2)
    +			require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url)
    +		})
    +		t.Run("Should add two includes when the user is a viewer", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleViewer
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
     	})
    -	t.Run("Should add one include when the user is an editor without catalog read", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleEditor
    -		service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
     
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    +	t.Run("With plugin RBAC - Enforce action first", func(t *testing.T) {
    +		t.Run("Should not see any includes with no app access", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:not-the-test-app1"}},
    +			}
    +			user.OrgRole = roletype.RoleNone
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			require.Len(t, treeRoot.Children, 0)
    +		})
    +		t.Run("Should only see the announcements as a none role user with app access", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}},
    +			}
    +			user.OrgRole = roletype.RoleNone
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
    +		t.Run("Should now see the catalog as a viewer with catalog read", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}, catalogReadAction: []string{}},
    +			}
    +			user.OrgRole = roletype.RoleViewer
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 2)
    +			require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url)
    +		})
    +		t.Run("Should not see the catalog include as an editor without catalog read", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleEditor
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
     	})
     }
    
  • pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go+40 0 modified
    @@ -3,6 +3,7 @@ package pluginaccesscontrol
     import (
     	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
    +	"github.com/grafana/grafana/pkg/services/datasources"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
     	"github.com/grafana/grafana/pkg/services/org"
     	"github.com/grafana/grafana/pkg/setting"
    @@ -75,3 +76,42 @@ func DeclareRBACRoles(service ac.Service, cfg *setting.Cfg, features featuremgmt
     
     	return service.DeclareFixedRoles(AppPluginsReader, PluginsWriter, PluginsMaintainer)
     }
    +
    +var datasourcesActions = map[string]bool{
    +	datasources.ActionIDRead:                    true,
    +	datasources.ActionQuery:                     true,
    +	datasources.ActionRead:                      true,
    +	datasources.ActionWrite:                     true,
    +	datasources.ActionDelete:                    true,
    +	datasources.ActionPermissionsRead:           true,
    +	datasources.ActionPermissionsWrite:          true,
    +	"datasources.caching:read":                  true,
    +	"datasources.caching:write":                 true,
    +	ac.ActionAlertingRuleExternalRead:           true,
    +	ac.ActionAlertingRuleExternalWrite:          true,
    +	ac.ActionAlertingInstancesExternalRead:      true,
    +	ac.ActionAlertingInstancesExternalWrite:     true,
    +	ac.ActionAlertingNotificationsExternalRead:  true,
    +	ac.ActionAlertingNotificationsExternalWrite: true,
    +}
    +
    +// GetDataSourceRouteEvaluator returns an evaluator for the given data source UID and action.
    +func GetDataSourceRouteEvaluator(dsUID, action string) ac.Evaluator {
    +	if datasourcesActions[action] {
    +		return ac.EvalPermission(action, "datasources:uid:"+dsUID)
    +	}
    +	return ac.EvalPermission(action)
    +}
    +
    +var pluginsActions = map[string]bool{
    +	ActionWrite:     true,
    +	ActionAppAccess: true,
    +}
    +
    +// GetPluginRouteEvaluator returns an evaluator for the given plugin ID and action.
    +func GetPluginRouteEvaluator(pluginID, action string) ac.Evaluator {
    +	if pluginsActions[action] {
    +		return ac.EvalPermission(action, "plugins:id:"+pluginID)
    +	}
    +	return ac.EvalPermission(action)
    +}
    
9cdba084a910

RBAC: Allow plugins to use scoped actions (#90945)

https://github.com/grafana/grafanaKevin MinehartJul 25, 2024via ghsa
8 files changed · +246 86
  • pkg/api/pluginproxy/ds_proxy.go+5 5 modified
    @@ -19,11 +19,11 @@ import (
     	glog "github.com/grafana/grafana/pkg/infra/log"
     	"github.com/grafana/grafana/pkg/infra/tracing"
     	"github.com/grafana/grafana/pkg/plugins"
    -	"github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
     	"github.com/grafana/grafana/pkg/services/datasources"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
     	"github.com/grafana/grafana/pkg/services/oauthtoken"
    +	pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
     	"github.com/grafana/grafana/pkg/setting"
     	"github.com/grafana/grafana/pkg/util"
     	"github.com/grafana/grafana/pkg/util/proxyutil"
    @@ -341,12 +341,12 @@ func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool {
     	ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
     	useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
     	if useRBAC {
    -		routeEval := accesscontrol.EvalPermission(route.ReqAction)
    -		ok := routeEval.Evaluate(proxy.ctx.GetPermissions())
    -		if !ok {
    +		routeEval := pluginac.GetDataSourceRouteEvaluator(proxy.ds.UID, route.ReqAction)
    +		hasAccess := routeEval.Evaluate(proxy.ctx.GetPermissions())
    +		if !hasAccess {
     			ctxLogger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path, "action", route.ReqAction, "path", route.Path, "method", route.Method)
     		}
    -		return ok
    +		return hasAccess
     	}
     	if route.ReqRole.IsValid() {
     		if hasUserRole := proxy.ctx.HasUserRole(route.ReqRole); !hasUserRole {
    
  • pkg/api/pluginproxy/ds_proxy_test.go+55 1 modified
    @@ -108,9 +108,18 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
     				Path: "mypath",
     				URL:  "https://example.com/api/v1/",
     			},
    +			{
    +				Path:      "api/rbac-home",
    +				ReqAction: "datasources:read",
    +			},
    +			{
    +				Path:      "api/rbac-restricted",
    +				ReqAction: "test-app.settings:read",
    +			},
     		}
     
     		ds := &datasources.DataSource{
    +			UID: "dsUID",
     			JsonData: simplejson.NewFromAny(map[string]any{
     				"clientId":   "asd",
     				"dynamicUrl": "https://dynamic.grafana.com",
    @@ -249,6 +258,51 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
     				require.NoError(t, err)
     			})
     		})
    +
    +		t.Run("plugin route with RBAC protection user is allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app.settings:read": nil}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.NoError(t, err)
    +		})
    +
    +		t.Run("plugin route with RBAC protection user is not allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app:read": nil}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.Error(t, err)
    +		})
    +
    +		t.Run("plugin route with dynamic RBAC protection user is allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:dsUID"}}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.NoError(t, err)
    +		})
    +
    +		t.Run("plugin route with dynamic RBAC protection user is not allowed", func(t *testing.T) {
    +			ctx, _ := setUp()
    +			ctx.SignedInUser.OrgID = int64(1)
    +			ctx.SignedInUser.OrgRole = org.RoleNone
    +			// Has access but to another app
    +			ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:notTheDsUID"}}}
    +			proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
    +			require.NoError(t, err)
    +			err = proxy.validateRequest()
    +			require.Error(t, err)
    +		})
     	})
     
     	t.Run("Plugin with multiple routes for token auth", func(t *testing.T) {
    @@ -1021,7 +1075,7 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
     	cfg := setting.NewCfg()
     	secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
     	secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
    -	features := featuremgmt.WithFeatures()
    +	features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
     	dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
     		&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
     		plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
    
  • pkg/api/pluginproxy/pluginproxy.go+3 1 modified
    @@ -15,6 +15,7 @@ import (
     	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
     	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
     	"github.com/grafana/grafana/pkg/services/secrets"
     	"github.com/grafana/grafana/pkg/setting"
    @@ -130,7 +131,8 @@ func (proxy *PluginProxy) HandleRequest() {
     func (proxy *PluginProxy) hasAccessToRoute(route *plugins.Route) bool {
     	useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
     	if useRBAC {
    -		hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(ac.EvalPermission(route.ReqAction))
    +		routeEval := pluginac.GetPluginRouteEvaluator(proxy.ps.PluginID, route.ReqAction)
    +		hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(routeEval)
     		if !hasAccess {
     			proxy.ctx.Logger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path)
     		}
    
  • pkg/api/pluginproxy/pluginproxy_test.go+21 2 modified
    @@ -454,7 +454,13 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     			Path:      "projects",
     			Method:    "GET",
     			URL:       "http://localhost/api/projects",
    -			ReqAction: "plugin-id.projects:read", // Protected by RBAC action
    +			ReqAction: "test-app.projects:read", // Protected by RBAC action
    +		},
    +		{
    +			Path:      "home",
    +			Method:    "GET",
    +			URL:       "http://localhost/api/home",
    +			ReqAction: "plugins.app:access", // Protected by RBAC action with plugin scope
     		},
     	}
     
    @@ -479,7 +485,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     		},
     		{
     			proxyPath:       "/projects",
    -			usrPerms:        map[string][]string{"plugin-id.projects:read": {}},
    +			usrPerms:        map[string][]string{"test-app.projects:read": {}},
     			expectedURLPath: "/api/projects",
     			expectedStatus:  http.StatusOK,
     		},
    @@ -489,6 +495,18 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     			expectedURLPath: "/api/projects",
     			expectedStatus:  http.StatusForbidden,
     		},
    +		{
    +			proxyPath:       "/home",
    +			usrPerms:        map[string][]string{"plugins.app:access": {"plugins:id:not-the-test-app"}},
    +			expectedURLPath: "/api/home",
    +			expectedStatus:  http.StatusForbidden,
    +		},
    +		{
    +			proxyPath:       "/home",
    +			usrPerms:        map[string][]string{"plugins.app:access": {"plugins:id:test-app"}},
    +			expectedURLPath: "/api/home",
    +			expectedStatus:  http.StatusOK,
    +		},
     	}
     
     	for _, tc := range tcs {
    @@ -533,6 +551,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
     				},
     			}
     			ps := &pluginsettings.DTO{
    +				PluginID:       "test-app",
     				SecureJSONData: map[string][]byte{},
     			}
     			cfg := &setting.Cfg{}
    
  • pkg/middleware/auth.go+1 1 modified
    @@ -127,7 +127,7 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, fea
     
     			if normalizeIncludePath(u.Path) == path {
     				useRBAC := features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && i.RequiresRBACAction()
    -				if useRBAC && !hasAccess(ac.EvalPermission(i.Action)) {
    +				if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) {
     					logger.Debug("Plugin include is covered by RBAC, user doesn't have access", "plugin", pluginID, "include", i.Name)
     					permitted = false
     					break
    
  • pkg/services/navtree/navtreeimpl/applinks.go+1 1 modified
    @@ -269,7 +269,7 @@ func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID st
     	hasAccess := ac.HasAccess(s.accessControl, c)
     	return func(include *plugins.Includes) bool {
     		useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
    -		if useRBAC && !hasAccess(ac.EvalPermission(include.Action)) {
    +		if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) {
     			s.log.Debug("plugin include is covered by RBAC, user doesn't have access",
     				"plugin", pluginID,
     				"include", include.Name)
    
  • pkg/services/navtree/navtreeimpl/applinks_test.go+120 75 modified
    @@ -406,20 +406,28 @@ func TestAddAppLinksAccessControl(t *testing.T) {
     			ID: "test-app1", Name: "Test app1 name", Type: plugins.TypeApp,
     			Includes: []*plugins.Includes{
     				{
    -					Name:       "Catalog",
    -					Path:       "/a/test-app1/catalog",
    +					Name:       "Home",
    +					Path:       "/a/test-app1/home",
     					Type:       "page",
     					AddToNav:   true,
     					DefaultNav: true,
    -					Role:       roletype.RoleEditor,
    -					Action:     catalogReadAction,
    +					Role:       roletype.RoleViewer,
     				},
     				{
    -					Name:     "Page2",
    -					Path:     "/a/test-app1/page2",
    +					Name:     "Catalog",
    +					Path:     "/a/test-app1/catalog",
    +					Type:     "page",
    +					AddToNav: true,
    +					Role:     roletype.RoleEditor,
    +					Action:   catalogReadAction,
    +				},
    +				{
    +					Name:     "Announcements",
    +					Path:     "/a/test-app1/announcements",
     					Type:     "page",
     					AddToNav: true,
     					Role:     roletype.RoleViewer,
    +					Action:   pluginaccesscontrol.ActionAppAccess,
     				},
     			},
     		},
    @@ -442,77 +450,114 @@ func TestAddAppLinksAccessControl(t *testing.T) {
     		},
     	}
     
    -	t.Run("Should not add app links when the user cannot access app plugins", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{}
    -		user.OrgRole = roletype.RoleAdmin
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		require.Len(t, treeRoot.Children, 0)
    -	})
    -	t.Run("Should add both includes when the user is an editor", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleEditor
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Url)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    -	})
    -	t.Run("Should add one include when the user is a viewer", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleViewer
    -
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    -	})
    -	t.Run("Should add both includes when the user is a viewer with catalog read", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}, catalogReadAction: []string{}},
    -		}
    -		user.OrgRole = roletype.RoleViewer
    -		service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +	t.Run("Without plugin RBAC - Enforce role", func(t *testing.T) {
    +		t.Run("Should not add app links when the user cannot access app plugins", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{}
    +			user.OrgRole = roletype.RoleAdmin
     
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Url)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			require.Len(t, treeRoot.Children, 0)
    +		})
    +		t.Run(" Should add all includes when the user is an editor", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleEditor
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 2)
    +			require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url)
    +		})
    +		t.Run("Should add two includes when the user is a viewer", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleViewer
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
     	})
    -	t.Run("Should add one include when the user is an editor without catalog read", func(t *testing.T) {
    -		treeRoot := navtree.NavTreeRoot{}
    -		user.Permissions = map[int64]map[string][]string{
    -			1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    -		}
    -		user.OrgRole = roletype.RoleEditor
    -		service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
     
    -		err := service.addAppLinks(&treeRoot, reqCtx)
    -		require.NoError(t, err)
    -		appsNode := treeRoot.FindById(navtree.NavIDApps)
    -		require.Len(t, appsNode.Children, 1)
    -		require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    -		require.Len(t, appsNode.Children[0].Children, 1)
    -		require.Equal(t, "/a/test-app1/page2", appsNode.Children[0].Children[0].Url)
    +	t.Run("With plugin RBAC - Enforce action first", func(t *testing.T) {
    +		t.Run("Should not see any includes with no app access", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:not-the-test-app1"}},
    +			}
    +			user.OrgRole = roletype.RoleNone
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			require.Len(t, treeRoot.Children, 0)
    +		})
    +		t.Run("Should only see the announcements as a none role user with app access", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}},
    +			}
    +			user.OrgRole = roletype.RoleNone
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
    +		t.Run("Should now see the catalog as a viewer with catalog read", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}, catalogReadAction: []string{}},
    +			}
    +			user.OrgRole = roletype.RoleViewer
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 2)
    +			require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url)
    +		})
    +		t.Run("Should not see the catalog include as an editor without catalog read", func(t *testing.T) {
    +			treeRoot := navtree.NavTreeRoot{}
    +			user.Permissions = map[int64]map[string][]string{
    +				1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}},
    +			}
    +			user.OrgRole = roletype.RoleEditor
    +			service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
    +
    +			err := service.addAppLinks(&treeRoot, reqCtx)
    +			require.NoError(t, err)
    +			appsNode := treeRoot.FindById(navtree.NavIDApps)
    +			require.Len(t, appsNode.Children, 1)
    +			require.Equal(t, "Test app1 name", appsNode.Children[0].Text)
    +			require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url)
    +			require.Len(t, appsNode.Children[0].Children, 1)
    +			require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url)
    +		})
     	})
     }
    
  • pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go+40 0 modified
    @@ -3,6 +3,7 @@ package pluginaccesscontrol
     import (
     	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
     	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
    +	"github.com/grafana/grafana/pkg/services/datasources"
     	"github.com/grafana/grafana/pkg/services/featuremgmt"
     	"github.com/grafana/grafana/pkg/services/org"
     	"github.com/grafana/grafana/pkg/setting"
    @@ -75,3 +76,42 @@ func DeclareRBACRoles(service ac.Service, cfg *setting.Cfg, features featuremgmt
     
     	return service.DeclareFixedRoles(AppPluginsReader, PluginsWriter, PluginsMaintainer)
     }
    +
    +var datasourcesActions = map[string]bool{
    +	datasources.ActionIDRead:                    true,
    +	datasources.ActionQuery:                     true,
    +	datasources.ActionRead:                      true,
    +	datasources.ActionWrite:                     true,
    +	datasources.ActionDelete:                    true,
    +	datasources.ActionPermissionsRead:           true,
    +	datasources.ActionPermissionsWrite:          true,
    +	"datasources.caching:read":                  true,
    +	"datasources.caching:write":                 true,
    +	ac.ActionAlertingRuleExternalRead:           true,
    +	ac.ActionAlertingRuleExternalWrite:          true,
    +	ac.ActionAlertingInstancesExternalRead:      true,
    +	ac.ActionAlertingInstancesExternalWrite:     true,
    +	ac.ActionAlertingNotificationsExternalRead:  true,
    +	ac.ActionAlertingNotificationsExternalWrite: true,
    +}
    +
    +// GetDataSourceRouteEvaluator returns an evaluator for the given data source UID and action.
    +func GetDataSourceRouteEvaluator(dsUID, action string) ac.Evaluator {
    +	if datasourcesActions[action] {
    +		return ac.EvalPermission(action, "datasources:uid:"+dsUID)
    +	}
    +	return ac.EvalPermission(action)
    +}
    +
    +var pluginsActions = map[string]bool{
    +	ActionWrite:     true,
    +	ActionAppAccess: true,
    +}
    +
    +// GetPluginRouteEvaluator returns an evaluator for the given plugin ID and action.
    +func GetPluginRouteEvaluator(pluginID, action string) ac.Evaluator {
    +	if pluginsActions[action] {
    +		return ac.EvalPermission(action, "plugins:id:"+pluginID)
    +	}
    +	return ac.EvalPermission(action)
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.