diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index b6b79715c66..ed3aa9cd33c 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -83,7 +83,7 @@ The following tables list permissions associated with basic and fixed roles. | `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | | `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | | `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | -| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. | +| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. | | `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | | `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | | `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | diff --git a/pkg/api/api.go b/pkg/api/api.go index a81358d1084..0196f234820 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -394,8 +394,8 @@ func (hs *HTTPServer) registerRoutes() { if hs.Cfg.PluginAdminEnabled && (hs.Features.IsEnabledGlobally(featuremgmt.FlagManagedPluginsInstall) || !hs.Cfg.PluginAdminExternalManageEnabled) { apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) { - pluginRoute.Post("/:pluginId/install", authorize(ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.InstallPlugin)) - pluginRoute.Post("/:pluginId/uninstall", authorize(ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.UninstallPlugin)) + pluginRoute.Post("/:pluginId/install", authorizeInOrg(ac.UseGlobalOrSingleOrg(hs.Cfg), ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.InstallPlugin)) + pluginRoute.Post("/:pluginId/uninstall", authorizeInOrg(ac.UseGlobalOrSingleOrg(hs.Cfg), ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.UninstallPlugin)) }) } diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index e30fc3bbcc5..6f4a383b0c9 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" @@ -43,23 +44,31 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { canInstall := []ac.Permission{{Action: pluginaccesscontrol.ActionInstall}} cannotInstall := []ac.Permission{{Action: "plugins:cannotinstall"}} + localOrg := int64(1) + globalOrg := int64(ac.GlobalOrgID) + type testCase struct { expectedCode int + permissionOrg int64 permissions []ac.Permission pluginAdminEnabled bool pluginAdminExternalManageEnabled bool + singleOrganization bool } tcs := []testCase{ - {expectedCode: http.StatusNotFound, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: true}, - {expectedCode: http.StatusNotFound, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: true}, - {expectedCode: http.StatusNotFound, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: false}, - {expectedCode: http.StatusForbidden, permissions: cannotInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false}, - {expectedCode: http.StatusOK, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false}, + {expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: true}, + {expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: true}, + {expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: false}, + {expectedCode: http.StatusForbidden, permissionOrg: globalOrg, permissions: cannotInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false}, + {expectedCode: http.StatusOK, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false}, + {expectedCode: http.StatusForbidden, permissionOrg: localOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false}, + {expectedCode: http.StatusForbidden, permissionOrg: localOrg, permissions: cannotInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false, singleOrganization: true}, + {expectedCode: http.StatusOK, permissionOrg: localOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false, singleOrganization: true}, } testName := func(action string, tc testCase) string { - return fmt.Sprintf("%s request returns %d when adminEnabled: %t, externalEnabled: %t, permissions: %q", - action, tc.expectedCode, tc.pluginAdminEnabled, tc.pluginAdminExternalManageEnabled, tc.permissions) + return fmt.Sprintf("%s request returns %d when adminEnabled: %t, externalEnabled: %t, permissions: %d: %s, single_org: %t", + action, tc.expectedCode, tc.pluginAdminEnabled, tc.pluginAdminExternalManageEnabled, tc.permissionOrg, tc.permissions[0].Action, tc.singleOrganization) } for _, tc := range tcs { @@ -67,15 +76,18 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { hs.Cfg = setting.NewCfg() hs.Cfg.PluginAdminEnabled = tc.pluginAdminEnabled hs.Cfg.PluginAdminExternalManageEnabled = tc.pluginAdminExternalManageEnabled + hs.Cfg.RBACSingleOrganization = tc.singleOrganization hs.orgService = &orgtest.FakeOrgService{ExpectedOrg: &org.Org{}} + hs.accesscontrolService = &actest.FakeService{} + hs.pluginInstaller = NewFakePluginInstaller() hs.pluginFileStore = &fakes.FakePluginFileStore{} }) t.Run(testName("Install", tc), func(t *testing.T) { input := strings.NewReader(`{"version": "1.0.2"}`) - req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(1, tc.permissions)) + req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(tc.permissionOrg, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) require.Equal(t, tc.expectedCode, res.StatusCode) @@ -84,7 +96,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { t.Run(testName("Uninstall", tc), func(t *testing.T) { input := strings.NewReader("{ }") - req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(1, tc.permissions)) + req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(tc.permissionOrg, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) require.Equal(t, tc.expectedCode, res.StatusCode) diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 9902f0146ea..573674b3e66 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -252,6 +252,16 @@ func UseGlobalOrg(c *contextmodel.ReqContext) (int64, error) { return GlobalOrgID, nil } +// UseGlobalOrSingleOrg returns the global organization or the current organization in a single organization setup +func UseGlobalOrSingleOrg(cfg *setting.Cfg) OrgIDGetter { + return func(c *contextmodel.ReqContext) (int64, error) { + if cfg.RBACSingleOrganization { + return c.GetOrgID(), nil + } + return GlobalOrgID, nil + } +} + // scopeParams holds the parameters used to fill in scope templates type scopeParams struct { OrgID int64 diff --git a/pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go b/pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go index de00dcb5e27..b462bc14ac1 100644 --- a/pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go +++ b/pkg/services/pluginsintegration/pluginaccesscontrol/accesscontrol.go @@ -59,7 +59,7 @@ func DeclareRBACRoles(service ac.Service, cfg *setting.Cfg) error { Role: ac.RoleDTO{ Name: ac.FixedRolePrefix + "plugins:maintainer", DisplayName: "Plugin Maintainer", - Description: "Install, uninstall plugins", + Description: "Install, uninstall plugins. Needs to be assigned globally.", Group: "Plugins", Permissions: []ac.Permission{ {Action: ActionInstall}, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9b675e00190..89222742b6c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -548,6 +548,8 @@ type Cfg struct { RBACPermissionValidationEnabled bool // Reset basic roles permissions on start-up RBACResetBasicRoles bool + // RBAC single organization. This configuration option is subject to change. + RBACSingleOrganization bool // GRPC Server. GRPCServerNetwork string @@ -1694,6 +1696,7 @@ func readAccessControlSettings(iniFile *ini.File, cfg *Cfg) { cfg.RBACPermissionCache = rbac.Key("permission_cache").MustBool(true) cfg.RBACPermissionValidationEnabled = rbac.Key("permission_validation_enabled").MustBool(false) cfg.RBACResetBasicRoles = rbac.Key("reset_basic_roles").MustBool(false) + cfg.RBACSingleOrganization = rbac.Key("single_organization").MustBool(false) } func readOAuth2ServerSettings(cfg *Cfg) {