RBAC: Add permissions to install and configure plugins (#51829)

* RBAC: Allow app plugins restriction

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* Moving declaration to HttpServer

Co-Authored-By: marefr <marcus.efraimsson@gmail.com>

* Picking changes from the other branch

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>

* Rename plugins.settings to plugins

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* Account for PluginAdminExternalManageEnabled

Co-authored-by: Will Browne <will.browne@grafana.com>

* Set metadata on instantiation

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: marefr <marcus.efraimsson@gmail.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: Jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Gabriel MABILLE
2022-09-09 09:44:50 +02:00
committed by GitHub
parent 8c081d4523
commit 101349fe49
21 changed files with 397 additions and 53 deletions

View File

@@ -43,7 +43,7 @@ var (
// that HTTPServer needs
func (hs *HTTPServer) declareFixedRoles() error {
// Declare plugins roles
if err := plugins.DeclareRBACRoles(hs.accesscontrolService); err != nil {
if err := plugins.DeclareRBACRoles(hs.accesscontrolService, hs.Cfg); err != nil {
return err
}

View File

@@ -369,16 +369,16 @@ func (hs *HTTPServer) registerRoutes() {
if hs.Cfg.PluginAdminEnabled && !hs.Cfg.PluginAdminExternalManageEnabled {
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Post("/:pluginId/install", routing.Wrap(hs.InstallPlugin))
pluginRoute.Post("/:pluginId/uninstall", routing.Wrap(hs.UninstallPlugin))
}, reqGrafanaAdmin)
pluginRoute.Post("/:pluginId/install", authorize(reqGrafanaAdmin, ac.EvalPermission(plugins.ActionInstall)), routing.Wrap(hs.InstallPlugin))
pluginRoute.Post("/:pluginId/uninstall", authorize(reqGrafanaAdmin, ac.EvalPermission(plugins.ActionInstall)), routing.Wrap(hs.UninstallPlugin))
})
}
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", routing.Wrap(hs.GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", routing.Wrap(hs.UpdatePluginSetting))
pluginRoute.Get("/:pluginId/metrics", routing.Wrap(hs.CollectPluginMetrics))
}, reqOrgAdmin)
pluginRoute.Get("/:pluginId/dashboards/", reqOrgAdmin, routing.Wrap(hs.GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", authorize(reqOrgAdmin, ac.EvalPermission(plugins.ActionWrite, pluginIDScope)), routing.Wrap(hs.UpdatePluginSetting))
pluginRoute.Get("/:pluginId/metrics", reqOrgAdmin, routing.Wrap(hs.CollectPluginMetrics))
})
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
apiRoute.Any("/datasources/proxy/:id/*", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequest)

View File

@@ -2,6 +2,7 @@ package dtos
import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type PluginSetting struct {
@@ -42,6 +43,7 @@ type PluginListItem struct {
Signature plugins.SignatureStatus `json:"signature"`
SignatureType plugins.SignatureType `json:"signatureType"`
SignatureOrg string `json:"signatureOrg"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
}
type PluginList []PluginListItem

View File

@@ -2,8 +2,11 @@ package api
import (
"context"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsettings"
)
type fakePluginManager struct {
@@ -44,6 +47,9 @@ func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.Pl
func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
var result []plugins.PluginDTO
if len(pluginTypes) == 0 {
pluginTypes = plugins.PluginTypes
}
for _, v := range pr.plugins {
for _, t := range pluginTypes {
if v.Type == t {
@@ -72,3 +78,66 @@ type fakePluginStaticRouteResolver struct {
func (psrr *fakePluginStaticRouteResolver) Routes() []*plugins.StaticRoute {
return psrr.routes
}
type fakePluginSettings struct {
pluginsettings.Service
plugins map[string]*pluginsettings.DTO
}
// GetPluginSettings returns all Plugin Settings for the provided Org
func (ps *fakePluginSettings) GetPluginSettings(ctx context.Context, args *pluginsettings.GetArgs) ([]*pluginsettings.DTO, error) {
res := []*pluginsettings.DTO{}
for _, dto := range ps.plugins {
res = append(res, dto)
}
return res, nil
}
// GetPluginSettingByPluginID returns a Plugin Settings by Plugin ID
func (ps *fakePluginSettings) GetPluginSettingByPluginID(ctx context.Context, args *pluginsettings.GetByPluginIDArgs) (*pluginsettings.DTO, error) {
if res, ok := ps.plugins[args.PluginID]; ok {
return res, nil
}
return nil, models.ErrPluginSettingNotFound
}
// UpdatePluginSetting updates a Plugin Setting
func (ps *fakePluginSettings) UpdatePluginSetting(ctx context.Context, args *pluginsettings.UpdateArgs) error {
var secureData map[string][]byte
if args.SecureJSONData != nil {
secureData := map[string][]byte{}
for k, v := range args.SecureJSONData {
secureData[k] = ([]byte)(v)
}
}
// save
ps.plugins[args.PluginID] = &pluginsettings.DTO{
ID: int64(len(ps.plugins)),
OrgID: args.OrgID,
PluginID: args.PluginID,
PluginVersion: args.PluginVersion,
JSONData: args.JSONData,
SecureJSONData: secureData,
Enabled: args.Enabled,
Pinned: args.Pinned,
Updated: time.Now(),
}
return nil
}
// UpdatePluginSettingPluginVersion updates a Plugin Setting's plugin version
func (ps *fakePluginSettings) UpdatePluginSettingPluginVersion(ctx context.Context, args *pluginsettings.UpdatePluginVersionArgs) error {
if res, ok := ps.plugins[args.PluginID]; ok {
res.PluginVersion = args.PluginVersion
return nil
}
return models.ErrPluginSettingNotFound
}
// DecryptedValues decrypts the encrypted secureJSONData of the provided plugin setting and
// returns the decrypted values.
func (ps *fakePluginSettings) DecryptedValues(dto *pluginsettings.DTO) map[string]string {
// TODO: Implement
return nil
}

View File

@@ -65,7 +65,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/plugindashboards"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/provisioning"
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
@@ -169,7 +169,7 @@ type HTTPServer struct {
commentsService *comments.Service
AlertNotificationService *alerting.AlertNotificationService
dashboardsnapshotsService dashboardsnapshots.Service
PluginSettings *pluginSettings.Service
PluginSettings pluginSettings.Service
AvatarCacheServer *avatar.AvatarCacheServer
preferenceService pref.Service
Csrf csrf.Service
@@ -218,7 +218,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings *pluginSettings.Service,
dashboardsnapshotsService dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings pluginSettings.Service,
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,

View File

@@ -384,7 +384,8 @@ func (hs *HTTPServer) setupConfigNodes(c *models.ReqContext) ([]*dtos.NavLink, e
})
}
if c.OrgRole == org.RoleAdmin || (hs.Cfg.PluginAdminEnabled && ac.ReqGrafanaAdmin(c)) {
// FIXME: while we don't have a permissions for listing plugins the legacy check has to stay as a default
if plugins.ReqCanAdminPlugins(hs.Cfg)(c) || hasAccess(plugins.ReqCanAdminPlugins(hs.Cfg), plugins.AdminAccessEvaluator) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Plugins",
Id: "plugins",

View File

@@ -13,10 +13,6 @@ import (
"sort"
"strings"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@@ -26,6 +22,9 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/plugins/storage"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
@@ -35,22 +34,33 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
// "" => no filter
// "0" => filter out core plugins
// "1" => filter out non-core plugins
coreFilter := c.Query("core")
// When using access control anyone that can create a data source should be able to list all data sources installed
// FIXME: while we don't have permissions for listing plugins we need this complex check:
// When using access control, should be able to list non-core plugins:
// * anyone that can create a data source
// * anyone that can install a plugin
// Fallback to only letting admins list non-core plugins
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
if !hasAccess(accesscontrol.ReqOrgAdmin, accesscontrol.EvalPermission(datasources.ActionCreate)) && !c.HasRole(org.RoleAdmin) {
coreFilter = "1"
}
reqOrgAdmin := ac.ReqHasRole(org.RoleAdmin)
hasAccess := ac.HasAccess(hs.AccessControl, c)
canListNonCorePlugins := reqOrgAdmin(c) || hasAccess(reqOrgAdmin, ac.EvalAny(
ac.EvalPermission(datasources.ActionCreate),
ac.EvalPermission(plugins.ActionInstall),
))
pluginSettingsMap, err := hs.pluginSettings(c.Req.Context(), c.OrgID)
if err != nil {
return response.Error(500, "Failed to get list of plugins", err)
return response.Error(http.StatusInternalServerError, "Failed to get list of plugins", err)
}
result := make(dtos.PluginList, 0)
for _, pluginDef := range hs.pluginStore.Plugins(c.Req.Context()) {
// Filter plugins
pluginDefinitions := hs.pluginStore.Plugins(c.Req.Context())
filteredPluginDefinitions := []plugins.PluginDTO{}
filteredPluginIDs := map[string]bool{}
for _, pluginDef := range pluginDefinitions {
// filter out app sub plugins
if embeddedFilter == "0" && pluginDef.IncludedInAppID != "" {
continue
@@ -61,6 +71,17 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
continue
}
// FIXME: while we don't have permissions for listing plugins we need this complex check:
// When using access control, should be able to list non-core plugins:
// * anyone that can create a data source
// * anyone that can install a plugin
// Should be able to list this installed plugin:
// * anyone that can edit its settings
if !pluginDef.IsCorePlugin() && !canListNonCorePlugins && !hasAccess(reqOrgAdmin,
ac.EvalPermission(plugins.ActionWrite, plugins.ScopeProvider.GetResourceScope(pluginDef.ID))) {
continue
}
// filter on type
if typeFilter != "" && typeFilter != string(pluginDef.Type) {
continue
@@ -70,6 +91,29 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
continue
}
// filter out built in plugins
if pluginDef.BuiltIn {
continue
}
// filter out disabled plugins
if pluginSetting, exists := pluginSettingsMap[pluginDef.ID]; exists {
if enabledFilter == "1" && !pluginSetting.Enabled {
continue
}
}
filteredPluginDefinitions = append(filteredPluginDefinitions, pluginDef)
filteredPluginIDs[pluginDef.ID] = true
}
// Compute metadata
pluginsMetadata := hs.getMultiAccessControlMetadata(c, c.OrgID,
plugins.ScopeProvider.GetResourceScope(""), filteredPluginIDs)
// Prepare DTO
result := make(dtos.PluginList, 0)
for _, pluginDef := range filteredPluginDefinitions {
listItem := dtos.PluginListItem{
Id: pluginDef.ID,
Name: pluginDef.Name,
@@ -82,6 +126,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
Signature: pluginDef.Signature,
SignatureType: pluginDef.SignatureType,
SignatureOrg: pluginDef.SignatureOrg,
AccessControl: pluginsMetadata[pluginDef.ID],
}
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID)
@@ -99,16 +144,6 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
listItem.DefaultNavUrl = hs.Cfg.AppSubURL + "/plugins/" + listItem.Id + "/"
}
// filter out disabled plugins
if enabledFilter == "1" && !listItem.Enabled {
continue
}
// filter out built in plugins
if pluginDef.BuiltIn {
continue
}
result = append(result, listItem)
}
@@ -127,9 +162,9 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
// In a first iteration, we only have one permission for app plugins.
// We will need a different permission to allow users to configure the plugin without needing access to it.
if plugin.IsApp() {
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
if !hasAccess(accesscontrol.ReqSignedIn,
accesscontrol.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
hasAccess := ac.HasAccess(hs.AccessControl, c)
if !hasAccess(ac.ReqSignedIn,
ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
return response.Error(http.StatusForbidden, "Access Denied", nil)
}
}

View File

@@ -16,12 +16,16 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
@@ -97,6 +101,56 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
}
}
func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) {
canInstall := []ac.Permission{{Action: plugins.ActionInstall}}
cannotInstall := []ac.Permission{{Action: "plugins:cannotinstall"}}
type testCase struct {
expectedCode int
permissions []ac.Permission
pluginAdminEnabled bool
pluginAdminExternalManageEnabled 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},
}
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)
}
pm := &fakePluginManager{
plugins: make(map[string]fakePlugin),
}
for _, tc := range tcs {
sc := setupHTTPServerWithCfg(t, true, &setting.Cfg{
RBACEnabled: true,
PluginAdminEnabled: tc.pluginAdminEnabled,
PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled})
setInitCtxSignedInViewer(sc.initCtx)
setAccessControlPermissions(sc.acmock, tc.permissions, sc.initCtx.OrgID)
sc.hs.pluginManager = pm
t.Run(testName("Install", tc), func(t *testing.T) {
input := strings.NewReader("{ \"version\": \"1.0.2\" }")
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/install", input, t)
assert.Equal(t, tc.expectedCode, response.Code)
})
t.Run(testName("Uninstall", tc), func(t *testing.T) {
input := strings.NewReader("{ }")
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/uninstall", input, t)
assert.Equal(t, tc.expectedCode, response.Code)
})
}
}
func Test_GetPluginAssets(t *testing.T) {
pluginID := "test-plugin"
pluginDir := "."
@@ -342,3 +396,100 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData
return backend.NewQueryDataResponse(), nil
}
func Test_PluginsList_AccessControl(t *testing.T) {
pluginStore := fakePluginStore{plugins: map[string]plugins.PluginDTO{
"test-app": {
PluginDir: "/grafana/plugins/test-app/dist",
Class: "external",
DefaultNavURL: "/plugins/test-app/page/test",
Pinned: false,
Signature: "unsigned",
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
JSONData: plugins.JSONData{
ID: "test-app",
Type: "app",
Name: "test-app",
Info: plugins.Info{
Version: "1.0.0",
},
},
},
"mysql": {
PluginDir: "/grafana/public/app/plugins/datasource/mysql",
Class: "core",
Pinned: false,
Signature: "internal",
Module: "app/plugins/datasource/mysql/module",
BaseURL: "public/app/plugins/datasource/mysql",
JSONData: plugins.JSONData{
ID: "mysql",
Type: "datasource",
Name: "MySQL",
Info: plugins.Info{
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"},
Description: "Data source for MySQL databases",
},
},
},
}}
pluginSettings := fakePluginSettings{plugins: map[string]*pluginsettings.DTO{
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true},
"mysql": {ID: 0, OrgID: 1, PluginID: "mysql", PluginVersion: "", Enabled: true}},
}
type testCase struct {
expectedCode int
role org.RoleType
isGrafanaAdmin bool
expectedPlugins []string
filters map[string]string
}
tcs := []testCase{
{expectedCode: http.StatusOK, role: org.RoleViewer, expectedPlugins: []string{"mysql"}},
{expectedCode: http.StatusOK, role: org.RoleViewer, isGrafanaAdmin: true, expectedPlugins: []string{"mysql", "test-app"}},
{expectedCode: http.StatusOK, role: org.RoleAdmin, expectedPlugins: []string{"mysql", "test-app"}},
}
testName := func(tc testCase) string {
return fmt.Sprintf("List request returns %d when role: %s, isGrafanaAdmin: %t, filters: %v",
tc.expectedCode, tc.role, tc.isGrafanaAdmin, tc.filters)
}
testUser := func(role org.RoleType, isGrafanaAdmin bool) user.SignedInUser {
return user.SignedInUser{
UserID: 2,
OrgID: 2,
OrgName: "TestOrg2",
OrgRole: role,
Login: "testUser",
Name: "testUser",
Email: "testUser@example.org",
OrgCount: 1,
IsGrafanaAdmin: isGrafanaAdmin,
IsAnonymous: false,
}
}
for _, tc := range tcs {
sc := setupHTTPServer(t, true)
sc.hs.PluginSettings = &pluginSettings
sc.hs.pluginStore = pluginStore
sc.hs.pluginsUpdateChecker = updatechecker.ProvidePluginsService(sc.hs.Cfg, pluginStore)
setInitCtxSignedInUser(sc.initCtx, testUser(tc.role, tc.isGrafanaAdmin))
t.Run(testName(tc), func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, "/api/plugins/", nil, t)
require.Equal(t, tc.expectedCode, response.Code)
var res dtos.PluginList
err := json.NewDecoder(response.Body).Decode(&res)
require.NoError(t, err)
require.Len(t, res, len(tc.expectedPlugins))
for _, plugin := range res {
require.Contains(t, tc.expectedPlugins, plugin.Id)
}
})
}
}

View File

@@ -7,7 +7,7 @@ import (
)
// The following variables cannot be constants, since they can be overridden through the -X link flag
var version = "7.5.0"
var version = "9.2.0"
var commit = "NA"
var buildBranch = "main"
var buildstamp string

View File

@@ -1,19 +1,35 @@
package plugins
import (
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
)
const (
// Plugins actions
ActionInstall = "plugins:install"
ActionWrite = "plugins:write"
// App Plugins actions
ActionAppAccess = "plugins.app:access"
)
var (
ScopeProvider = ac.NewScopeProvider("plugins")
// Protects access to the Configuration > Plugins page
AdminAccessEvaluator = ac.EvalAny(ac.EvalPermission(ActionWrite), ac.EvalPermission(ActionInstall))
)
func DeclareRBACRoles(service ac.Service) error {
func ReqCanAdminPlugins(cfg *setting.Cfg) func(rc *models.ReqContext) bool {
// Legacy handler that protects access to the Configuration > Plugins page
return func(rc *models.ReqContext) bool {
return rc.OrgRole == org.RoleAdmin || cfg.PluginAdminEnabled && rc.IsGrafanaAdmin
}
}
func DeclareRBACRoles(service ac.Service, cfg *setting.Cfg) error {
AppPluginsReader := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: ac.FixedRolePrefix + "plugins.app:reader",
@@ -26,5 +42,34 @@ func DeclareRBACRoles(service ac.Service) error {
},
Grants: []string{string(org.RoleViewer)},
}
return service.DeclareFixedRoles(AppPluginsReader)
PluginsWriter := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: ac.FixedRolePrefix + "plugins:writer",
DisplayName: "Plugin Writer",
Description: "Enable and disable plugins and edit plugins' settings",
Group: "Plugins",
Permissions: []ac.Permission{
{Action: ActionWrite, Scope: ScopeProvider.GetResourceAllScope()},
},
},
Grants: []string{string(org.RoleAdmin)},
}
PluginsMaintainer := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: ac.FixedRolePrefix + "plugins:maintainer",
DisplayName: "Plugin Maintainer",
Description: "Install, uninstall plugins",
Group: "Plugins",
Permissions: []ac.Permission{
{Action: ActionInstall},
},
},
Grants: []string{ac.RoleGrafanaAdmin},
}
if !cfg.PluginAdminEnabled || cfg.PluginAdminExternalManageEnabled {
PluginsMaintainer.Grants = []string{}
}
return service.DeclareFixedRoles(AppPluginsReader, PluginsWriter, PluginsMaintainer)
}

View File

@@ -161,6 +161,12 @@ var ReqOrgAdminOrEditor = func(c *models.ReqContext) bool {
return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor
}
// ReqHasRole generates a fallback to check whether the user has a role
// Note that while ReqOrgAdmin returns false for a Grafana Admin / Viewer, ReqHasRole(org.RoleAdmin) will return true
func ReqHasRole(role org.RoleType) func(c *models.ReqContext) bool {
return func(c *models.ReqContext) bool { return c.HasRole(role) }
}
func BuildPermissionsMap(permissions []Permission) map[string]bool {
permissionsMap := make(map[string]bool)
for _, p := range permissions {

View File

@@ -48,11 +48,11 @@ func TestPlugins(t *testing.T) {
t.Run("Request is forbidden if not from an admin", func(t *testing.T) {
status, body := makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/install"))
assert.Equal(t, 403, status)
assert.Equal(t, "Permission denied", body["message"])
assert.Equal(t, "You'll need additional permissions to perform this action. Permissions needed: plugins:install", body["message"])
status, body = makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/uninstall"))
assert.Equal(t, 403, status)
assert.Equal(t, "Permission denied", body["message"])
assert.Equal(t, "You'll need additional permissions to perform this action. Permissions needed: plugins:install", body["message"])
})
t.Run("Request is not forbidden if from an admin", func(t *testing.T) {