mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 11:54:20 +08:00
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:
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user