Skip to content

Commit c326d86

Browse files
kminehartgamab
andauthored
RBAC: Allow plugins to use scoped actions (grafana#90946)
Co-authored-by: gamab <[email protected]>
1 parent 95000f9 commit c326d86

File tree

8 files changed

+246
-86
lines changed

8 files changed

+246
-86
lines changed

pkg/api/pluginproxy/ds_proxy.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import (
1919
glog "github.com/grafana/grafana/pkg/infra/log"
2020
"github.com/grafana/grafana/pkg/infra/tracing"
2121
"github.com/grafana/grafana/pkg/plugins"
22-
"github.com/grafana/grafana/pkg/services/accesscontrol"
2322
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2423
"github.com/grafana/grafana/pkg/services/datasources"
2524
"github.com/grafana/grafana/pkg/services/featuremgmt"
2625
"github.com/grafana/grafana/pkg/services/oauthtoken"
26+
pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
2727
"github.com/grafana/grafana/pkg/setting"
2828
"github.com/grafana/grafana/pkg/util"
2929
"github.com/grafana/grafana/pkg/util/proxyutil"
@@ -341,12 +341,12 @@ func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool {
341341
ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
342342
useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
343343
if useRBAC {
344-
routeEval := accesscontrol.EvalPermission(route.ReqAction)
345-
ok := routeEval.Evaluate(proxy.ctx.GetPermissions())
346-
if !ok {
344+
routeEval := pluginac.GetDataSourceRouteEvaluator(proxy.ds.UID, route.ReqAction)
345+
hasAccess := routeEval.Evaluate(proxy.ctx.GetPermissions())
346+
if !hasAccess {
347347
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)
348348
}
349-
return ok
349+
return hasAccess
350350
}
351351
if route.ReqRole.IsValid() {
352352
if hasUserRole := proxy.ctx.HasUserRole(route.ReqRole); !hasUserRole {

pkg/api/pluginproxy/ds_proxy_test.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,18 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
108108
Path: "mypath",
109109
URL: "https://example.com/api/v1/",
110110
},
111+
{
112+
Path: "api/rbac-home",
113+
ReqAction: "datasources:read",
114+
},
115+
{
116+
Path: "api/rbac-restricted",
117+
ReqAction: "test-app.settings:read",
118+
},
111119
}
112120

113121
ds := &datasources.DataSource{
122+
UID: "dsUID",
114123
JsonData: simplejson.NewFromAny(map[string]any{
115124
"clientId": "asd",
116125
"dynamicUrl": "https://dynamic.grafana.com",
@@ -249,6 +258,51 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
249258
require.NoError(t, err)
250259
})
251260
})
261+
262+
t.Run("plugin route with RBAC protection user is allowed", func(t *testing.T) {
263+
ctx, _ := setUp()
264+
ctx.SignedInUser.OrgID = int64(1)
265+
ctx.SignedInUser.OrgRole = identity.RoleNone
266+
ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app.settings:read": nil}}
267+
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
268+
require.NoError(t, err)
269+
err = proxy.validateRequest()
270+
require.NoError(t, err)
271+
})
272+
273+
t.Run("plugin route with RBAC protection user is not allowed", func(t *testing.T) {
274+
ctx, _ := setUp()
275+
ctx.SignedInUser.OrgID = int64(1)
276+
ctx.SignedInUser.OrgRole = identity.RoleNone
277+
ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"test-app:read": nil}}
278+
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-restricted")
279+
require.NoError(t, err)
280+
err = proxy.validateRequest()
281+
require.Error(t, err)
282+
})
283+
284+
t.Run("plugin route with dynamic RBAC protection user is allowed", func(t *testing.T) {
285+
ctx, _ := setUp()
286+
ctx.SignedInUser.OrgID = int64(1)
287+
ctx.SignedInUser.OrgRole = identity.RoleNone
288+
ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:dsUID"}}}
289+
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
290+
require.NoError(t, err)
291+
err = proxy.validateRequest()
292+
require.NoError(t, err)
293+
})
294+
295+
t.Run("plugin route with dynamic RBAC protection user is not allowed", func(t *testing.T) {
296+
ctx, _ := setUp()
297+
ctx.SignedInUser.OrgID = int64(1)
298+
ctx.SignedInUser.OrgRole = identity.RoleNone
299+
// Has access but to another app
300+
ctx.SignedInUser.Permissions = map[int64]map[string][]string{1: {"datasources:read": {"datasources:uid:notTheDsUID"}}}
301+
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/rbac-home")
302+
require.NoError(t, err)
303+
err = proxy.validateRequest()
304+
require.Error(t, err)
305+
})
252306
})
253307

254308
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
10211075
cfg := setting.NewCfg()
10221076
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
10231077
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
1024-
features := featuremgmt.WithFeatures()
1078+
features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
10251079
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
10261080
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
10271081
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))

pkg/api/pluginproxy/pluginproxy.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
1616
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
1717
"github.com/grafana/grafana/pkg/services/featuremgmt"
18+
pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
1819
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
1920
"github.com/grafana/grafana/pkg/services/secrets"
2021
"github.com/grafana/grafana/pkg/setting"
@@ -130,7 +131,8 @@ func (proxy *PluginProxy) HandleRequest() {
130131
func (proxy *PluginProxy) hasAccessToRoute(route *plugins.Route) bool {
131132
useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != ""
132133
if useRBAC {
133-
hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(ac.EvalPermission(route.ReqAction))
134+
routeEval := pluginac.GetPluginRouteEvaluator(proxy.ps.PluginID, route.ReqAction)
135+
hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(routeEval)
134136
if !hasAccess {
135137
proxy.ctx.Logger.Debug("plugin route is covered by RBAC, user doesn't have access", "route", proxy.ctx.Req.URL.Path)
136138
}

pkg/api/pluginproxy/pluginproxy_test.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,13 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
455455
Path: "projects",
456456
Method: "GET",
457457
URL: "http://localhost/api/projects",
458-
ReqAction: "plugin-id.projects:read", // Protected by RBAC action
458+
ReqAction: "test-app.projects:read", // Protected by RBAC action
459+
},
460+
{
461+
Path: "home",
462+
Method: "GET",
463+
URL: "http://localhost/api/home",
464+
ReqAction: "plugins.app:access", // Protected by RBAC action with plugin scope
459465
},
460466
}
461467

@@ -480,7 +486,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
480486
},
481487
{
482488
proxyPath: "/projects",
483-
usrPerms: map[string][]string{"plugin-id.projects:read": {}},
489+
usrPerms: map[string][]string{"test-app.projects:read": {}},
484490
expectedURLPath: "/api/projects",
485491
expectedStatus: http.StatusOK,
486492
},
@@ -490,6 +496,18 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
490496
expectedURLPath: "/api/projects",
491497
expectedStatus: http.StatusForbidden,
492498
},
499+
{
500+
proxyPath: "/home",
501+
usrPerms: map[string][]string{"plugins.app:access": {"plugins:id:not-the-test-app"}},
502+
expectedURLPath: "/api/home",
503+
expectedStatus: http.StatusForbidden,
504+
},
505+
{
506+
proxyPath: "/home",
507+
usrPerms: map[string][]string{"plugins.app:access": {"plugins:id:test-app"}},
508+
expectedURLPath: "/api/home",
509+
expectedStatus: http.StatusOK,
510+
},
493511
}
494512

495513
for _, tc := range tcs {
@@ -534,6 +552,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
534552
},
535553
}
536554
ps := &pluginsettings.DTO{
555+
PluginID: "test-app",
537556
SecureJSONData: map[string][]byte{},
538557
}
539558
cfg := &setting.Cfg{}

pkg/middleware/auth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, fea
127127

128128
if normalizeIncludePath(u.Path) == path {
129129
useRBAC := features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && i.RequiresRBACAction()
130-
if useRBAC && !hasAccess(ac.EvalPermission(i.Action)) {
130+
if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) {
131131
logger.Debug("Plugin include is covered by RBAC, user doesn't have access", "plugin", pluginID, "include", i.Name)
132132
permitted = false
133133
break

pkg/services/navtree/navtreeimpl/applinks.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID st
269269
hasAccess := ac.HasAccess(s.accessControl, c)
270270
return func(include *plugins.Includes) bool {
271271
useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
272-
if useRBAC && !hasAccess(ac.EvalPermission(include.Action)) {
272+
if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) {
273273
s.log.Debug("plugin include is covered by RBAC, user doesn't have access",
274274
"plugin", pluginID,
275275
"include", include.Name)

0 commit comments

Comments
 (0)