RBAC: Remove accessControlOnCall feature toggle (#101222)
* RBAC: Remove accessControlOnCall feature toggle * Leave the other one in place * Tests * frontend * Readd empty ft to frontend test * Remove legacy RBAC check * Fix test * no need for context * Remove unused variable * Remove unecessary param * remove unecessary param from tests * More tests :D
This commit is contained in:
@@ -28,7 +28,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `featureHighlights` | Highlight Grafana Enterprise features | |
|
||||
| `correlations` | Correlations page | Yes |
|
||||
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
|
||||
| `accessControlOnCall` | Access control primitives for OnCall | Yes |
|
||||
| `nestedFolders` | Enable folder nesting | Yes |
|
||||
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
|
||||
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
|
||||
|
||||
@@ -43,7 +43,6 @@ export interface FeatureToggles {
|
||||
cloudWatchCrossAccountQuerying?: boolean;
|
||||
showDashboardValidationWarnings?: boolean;
|
||||
mysqlAnsiQuotes?: boolean;
|
||||
accessControlOnCall?: boolean;
|
||||
nestedFolders?: boolean;
|
||||
alertingBacktesting?: boolean;
|
||||
editPanelCSVDragAndDrop?: boolean;
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqSignedInNoAnonymous := middleware.ReqSignedInNoAnonymous
|
||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
reqRoleForAppRoute := middleware.RoleAppPluginAuth(hs.AccessControl, hs.pluginStore, hs.Features, hs.log)
|
||||
reqRoleForAppRoute := middleware.RoleAppPluginAuth(hs.AccessControl, hs.pluginStore, hs.log)
|
||||
reqSnapshotPublicModeOrCreate := middleware.SnapshotPublicModeOrCreate(hs.Cfg, hs.AccessControl)
|
||||
reqSnapshotPublicModeOrDelete := middleware.SnapshotPublicModeOrDelete(hs.Cfg, hs.AccessControl)
|
||||
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
|
||||
|
||||
@@ -340,8 +340,7 @@ func (proxy *DataSourceProxy) validateRequest() error {
|
||||
|
||||
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 {
|
||||
if route.ReqAction != "" {
|
||||
routeEval := pluginac.GetDataSourceRouteEvaluator(proxy.ds.UID, route.ReqAction)
|
||||
hasAccess := routeEval.Evaluate(proxy.ctx.GetPermissions())
|
||||
if !hasAccess {
|
||||
|
||||
@@ -1094,7 +1094,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(featuremgmt.FlagAccessControlOnCall)
|
||||
features := featuremgmt.WithFeatures()
|
||||
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()))
|
||||
|
||||
@@ -129,8 +129,7 @@ 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 {
|
||||
if route.ReqAction != "" {
|
||||
routeEval := pluginac.GetPluginRouteEvaluator(proxy.ps.PluginID, route.ReqAction)
|
||||
hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(routeEval)
|
||||
if !hasAccess {
|
||||
|
||||
@@ -557,7 +557,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
|
||||
SecureJSONData: map[string][]byte{},
|
||||
}
|
||||
cfg := &setting.Cfg{}
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall))
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
proxy.HandleRequest()
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
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/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
@@ -138,9 +137,7 @@ func CanAdminPlugins(cfg *setting.Cfg, accessControl ac.AccessControl) func(c *c
|
||||
}
|
||||
}
|
||||
|
||||
func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, features featuremgmt.FeatureToggles,
|
||||
logger log.Logger,
|
||||
) func(c *contextmodel.ReqContext) {
|
||||
func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, logger log.Logger) func(c *contextmodel.ReqContext) {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
pluginID := web.Params(c.Req)[":id"]
|
||||
p, exists := ps.Plugin(c.Req.Context(), pluginID)
|
||||
@@ -164,12 +161,11 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, fea
|
||||
}
|
||||
|
||||
if normalizeIncludePath(u.Path) == path {
|
||||
useRBAC := features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && i.RequiresRBACAction()
|
||||
if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) {
|
||||
if i.RequiresRBACAction() && !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
|
||||
} else if !useRBAC && !c.HasUserRole(i.Role) {
|
||||
} else if !i.RequiresRBACAction() && !c.HasUserRole(i.Role) {
|
||||
permitted = false
|
||||
break
|
||||
}
|
||||
|
||||
@@ -204,11 +204,10 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
0: tc.role,
|
||||
},
|
||||
})
|
||||
features := featuremgmt.WithFeatures()
|
||||
logger := &logtest.Fake{}
|
||||
ac := &actest.FakeAccessControl{}
|
||||
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, features, logger), func(c *contextmodel.ReqContext) {
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, logger), func(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{})
|
||||
})
|
||||
sc.fakeReq("GET", path).exec()
|
||||
@@ -227,10 +226,9 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
0: org.RoleViewer,
|
||||
},
|
||||
})
|
||||
features := featuremgmt.WithFeatures()
|
||||
logger := &logtest.Fake{}
|
||||
ac := &actest.FakeAccessControl{}
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, &pluginstore.FakePluginStore{}, features, logger), func(c *contextmodel.ReqContext) {
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, &pluginstore.FakePluginStore{}, logger), func(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{})
|
||||
})
|
||||
sc.fakeReq("GET", "/a/test-app/test").exec()
|
||||
@@ -245,7 +243,6 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
0: org.RoleViewer,
|
||||
},
|
||||
})
|
||||
features := featuremgmt.WithFeatures()
|
||||
logger := &logtest.Fake{}
|
||||
ac := &actest.FakeAccessControl{}
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, pluginstore.NewFakePluginStore(pluginstore.Plugin{
|
||||
@@ -259,7 +256,7 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}), features, logger), func(c *contextmodel.ReqContext) {
|
||||
}), logger), func(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{})
|
||||
})
|
||||
sc.fakeReq("GET", "/a/test-app/notExistingPath").exec()
|
||||
@@ -307,7 +304,6 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
},
|
||||
})
|
||||
logger := &logtest.Fake{}
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
|
||||
ac := &actest.FakeAccessControl{
|
||||
ExpectedEvaluate: tc.evalResult,
|
||||
ExpectedErr: tc.evalErr,
|
||||
@@ -327,7 +323,7 @@ func TestRoleAppPluginAuth(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, features, logger), func(c *contextmodel.ReqContext) {
|
||||
sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, logger), func(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, map[string]interface{}{})
|
||||
})
|
||||
sc.fakeReq("GET", path).exec()
|
||||
|
||||
@@ -68,7 +68,7 @@ func ProvideService(
|
||||
lock,
|
||||
)
|
||||
|
||||
api.NewAccessControlAPI(routeRegister, accessControl, service, userService, features).RegisterAPIEndpoints()
|
||||
api.NewAccessControlAPI(routeRegister, accessControl, service, userService).RegisterAPIEndpoints()
|
||||
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -472,14 +472,9 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error {
|
||||
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments
|
||||
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeclarePluginRoles")
|
||||
_, span := tracer.Start(ctx, "accesscontrol.acimpl.DeclarePluginRoles")
|
||||
defer span.End()
|
||||
|
||||
// Protect behind feature toggle
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
||||
return nil
|
||||
}
|
||||
|
||||
acRegs := pluginutils.ToRegistrations(ID, name, regs)
|
||||
for _, r := range acRegs {
|
||||
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
|
||||
|
||||
@@ -253,7 +253,6 @@ func TestService_DeclarePluginRoles(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ac := setupTestEnv(t)
|
||||
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
|
||||
|
||||
// Reset the registations
|
||||
ac.registrations = accesscontrol.RegistrationList{}
|
||||
|
||||
@@ -17,20 +17,17 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/api")
|
||||
|
||||
func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service,
|
||||
userSvc user.Service, features featuremgmt.FeatureToggles) *AccessControlAPI {
|
||||
func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service, userSvc user.Service) *AccessControlAPI {
|
||||
return &AccessControlAPI{
|
||||
RouteRegister: router,
|
||||
Service: service,
|
||||
userSvc: userSvc,
|
||||
AccessControl: accesscontrol,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +36,6 @@ type AccessControlAPI struct {
|
||||
AccessControl ac.AccessControl
|
||||
RouteRegister routing.RouteRegister
|
||||
userSvc user.Service
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func (api *AccessControlAPI) RegisterAPIEndpoints() {
|
||||
@@ -48,9 +44,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() {
|
||||
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
|
||||
rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
|
||||
rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
|
||||
if api.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) {
|
||||
rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions))
|
||||
}
|
||||
rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions))
|
||||
}, requestmeta.SetOwner(requestmeta.TeamAuth))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -42,7 +41,7 @@ func TestAPI_getUserActions(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}, featuremgmt.WithFeatures())
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{})
|
||||
api.RegisterAPIEndpoints()
|
||||
|
||||
server := webtest.NewServer(t, api.RouteRegister)
|
||||
@@ -95,7 +94,7 @@ func TestAPI_getUserPermissions(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}, featuremgmt.WithFeatures())
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{})
|
||||
api.RegisterAPIEndpoints()
|
||||
|
||||
server := webtest.NewServer(t, api.RouteRegister)
|
||||
@@ -192,7 +191,7 @@ func TestAccessControlAPI_searchUsersPermissions(t *testing.T) {
|
||||
mockUserSvc := usertest.NewMockService(t)
|
||||
mockUserSvc.On("GetByUID", mock.Anything, &user.GetUserByUIDQuery{UID: "user_2_uid"}).Return(&user.User{ID: 2}, nil).Maybe()
|
||||
mockUserSvc.On("GetByUID", mock.Anything, &user.GetUserByUIDQuery{UID: "non_existent_uid"}).Return(nil, user.ErrUserNotFound).Maybe()
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), accessControl, acSvc, mockUserSvc, featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall))
|
||||
api := NewAccessControlAPI(routing.NewRouteRegister(), accessControl, acSvc, mockUserSvc)
|
||||
api.RegisterAPIEndpoints()
|
||||
|
||||
server := webtest.NewServer(t, api.RouteRegister)
|
||||
|
||||
@@ -583,7 +583,7 @@ func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string,
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
|
||||
defer span.End()
|
||||
|
||||
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
||||
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
|
||||
return nil
|
||||
}
|
||||
for _, reg := range registrations {
|
||||
|
||||
@@ -328,7 +328,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
tests := []actionSetTest{
|
||||
{
|
||||
desc: "should be able to register a plugin action set if the right feature toggles are enabled",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
@@ -345,7 +345,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "should not register plugin action set if feature toggles are missing",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
@@ -357,7 +357,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "should be able to register multiple plugin action sets",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
@@ -382,7 +382,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "action set actions should be added not replaced",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
@@ -425,7 +425,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register an action that doesn't have a plugin prefix",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
@@ -441,7 +441,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register action set that is not in the allow list",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
|
||||
@@ -198,14 +198,6 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
},
|
||||
{
|
||||
Name: "accessControlOnCall",
|
||||
Description: "Access control primitives for OnCall",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: identityAccessTeam,
|
||||
HideFromAdminPage: true,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
Name: "nestedFolders",
|
||||
Description: "Enable folder nesting",
|
||||
|
||||
@@ -24,7 +24,6 @@ grpcServer,preview,@grafana/search-and-storage,false,false,false
|
||||
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false
|
||||
showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false
|
||||
mysqlAnsiQuotes,experimental,@grafana/search-and-storage,false,false,false
|
||||
accessControlOnCall,GA,@grafana/identity-access-team,false,false,false
|
||||
nestedFolders,GA,@grafana/search-and-storage,false,false,false
|
||||
alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false
|
||||
editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true
|
||||
|
||||
|
@@ -107,10 +107,6 @@ const (
|
||||
// Use double quotes to escape keyword in a MySQL query
|
||||
FlagMysqlAnsiQuotes = "mysqlAnsiQuotes"
|
||||
|
||||
// FlagAccessControlOnCall
|
||||
// Access control primitives for OnCall
|
||||
FlagAccessControlOnCall = "accessControlOnCall"
|
||||
|
||||
// FlagNestedFolders
|
||||
// Enable folder nesting
|
||||
FlagNestedFolders = "nestedFolders"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"name": "accessControlOnCall",
|
||||
"resourceVersion": "1726562036211",
|
||||
"creationTimestamp": "2022-10-19T16:10:09Z",
|
||||
"deletionTimestamp": "2025-02-24T14:40:54Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-09-17 08:33:56.211355566 +0000 UTC"
|
||||
}
|
||||
|
||||
@@ -268,13 +268,12 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n
|
||||
func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
return func(include *plugins.Includes) bool {
|
||||
useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
|
||||
if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) {
|
||||
if include.RequiresRBACAction() && !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)
|
||||
return false
|
||||
} else if !useRBAC && !c.HasUserRole(include.Role) {
|
||||
} else if !include.RequiresRBACAction() && !c.HasUserRole(include.Role) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -450,114 +450,67 @@ func TestAddAppLinksAccessControl(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
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 = identity.RoleAdmin
|
||||
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 = identity.RoleNone
|
||||
service.features = featuremgmt.WithFeatures()
|
||||
|
||||
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 = identity.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 = identity.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)
|
||||
})
|
||||
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 = identity.RoleNone
|
||||
service.features = featuremgmt.WithFeatures()
|
||||
|
||||
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 = identity.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 = identity.RoleViewer
|
||||
service.features = featuremgmt.WithFeatures()
|
||||
|
||||
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 = identity.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.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 = identity.RoleEditor
|
||||
service.features = featuremgmt.WithFeatures()
|
||||
|
||||
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 = identity.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 = identity.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)
|
||||
})
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,9 +30,7 @@ jest.mock('../plugin_loader', () => ({
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
featureToggles: {
|
||||
accessControlOnCall: true,
|
||||
},
|
||||
featureToggles: {},
|
||||
apps: {},
|
||||
theme2: {
|
||||
breakpoints: {
|
||||
|
||||
@@ -135,7 +135,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
||||
}
|
||||
|
||||
// Check if action exists and give access if user has the required permission.
|
||||
if (pluginInclude?.action && config.featureToggles.accessControlOnCall) {
|
||||
if (pluginInclude?.action) {
|
||||
return contextSrv.hasPermission(pluginInclude.action);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user