RBAC: Allow role registration for plugins (#57387)

* Picking role registration from OnCall POC branch

* Fix test

* Remove include actions from this PR

* Removing unused permission

* Adding test to DeclarePluginRoles

* Add testcase to RegisterFixed role

* Additional test case

* Adding tests to validate plugins roles

* Add test to plugin loader

* Nit.

* Scuemata validation

* Changing the design to decouple accesscontrol from plugin management

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

* Fixing tests

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

* Add missing files

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

* Remove feature toggle check from loader

* Remove feature toggleimport

* Feedback

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

* Fix test'

* Make plugins.RoleRegistry interface typed

* Remove comment question

* No need for json tags anymore

* Nit. log

* Adding the schema validation

* Remove group to take plugin Name instead

* Revert sqlstore -> db

* Nit.

* Nit. on tests

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* Update pkg/services/accesscontrol/plugins.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Log message

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

* Log message

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

* Remove unecessary method. Update test name.

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* Fix linting

* Update cue descriptions

* Fix test

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Jguer <joao.guerreiro@grafana.com>
Co-authored-by: marefr <marcus.efraimsson@gmail.com>
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Gabriel MABILLE
2022-11-07 11:30:45 +01:00
committed by GitHub
co-authored by Kalle Persson Jguer marefr ievaVasiljeva
parent 334b498632
commit 30fae33f66
22 changed files with 852 additions and 32 deletions
+5
View File
@@ -74,3 +74,8 @@ type PluginLoaderAuthorizer interface {
// CanLoadPlugin confirms if a plugin is authorized to load
CanLoadPlugin(plugin *Plugin) bool
}
// RoleRegistry handles the plugin RBAC roles and their assignments
type RoleRegistry interface {
DeclarePluginRoles(ctx context.Context, ID, name string, registrations []RoleRegistration) error
}
+12
View File
@@ -355,3 +355,15 @@ func (*FakeLicensingService) EnabledFeatures() map[string]bool {
func (*FakeLicensingService) FeatureEnabled(_ string) bool {
return false
}
type FakeRoleRegistry struct {
ExpectedErr error
}
func NewFakeRoleRegistry() *FakeRoleRegistry {
return &FakeRoleRegistry{}
}
func (f *FakeRoleRegistry) DeclarePluginRoles(_ context.Context, _ string, _ string, _ []plugins.RoleRegistration) error {
return f.ExpectedErr
}
+10 -3
View File
@@ -42,6 +42,7 @@ type Loader struct {
pluginFinder finder.Finder
processManager process.Service
pluginRegistry registry.Service
roleRegistry plugins.RoleRegistry
pluginInitializer initializer.Initializer
signatureValidator signature.Validator
pluginStorage storage.Manager
@@ -51,14 +52,15 @@ type Loader struct {
}
func ProvideService(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider) *Loader {
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
roleRegistry plugins.RoleRegistry) *Loader {
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath))
storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath), roleRegistry)
}
func New(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, pluginStorage storage.Manager) *Loader {
processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry) *Loader {
return &Loader{
pluginFinder: finder.New(),
pluginRegistry: pluginRegistry,
@@ -68,6 +70,7 @@ func New(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoa
pluginStorage: pluginStorage,
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
roleRegistry: roleRegistry,
}
}
@@ -195,6 +198,10 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO
return nil, err
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil {
l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "path", p.PluginDir, "error", errDeclareRoles)
}
}
for _, p := range verifiedPlugins {
+107 -1
View File
@@ -597,6 +597,111 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
})
}
func TestLoader_Load_RBACReady(t *testing.T) {
pluginDir, err := filepath.Abs("../testdata/test-app-with-roles")
if err != nil {
t.Errorf("could not construct absolute path of current dir")
return
}
tests := []struct {
name string
cfg *config.Cfg
pluginPaths []string
appURL string
existingPlugins map[string]struct{}
want []*plugins.Plugin
}{
{
name: "Load plugin defining one RBAC role",
cfg: &config.Cfg{},
appURL: "http://localhost:3000",
pluginPaths: []string{"../testdata/test-app-with-roles"},
want: []*plugins.Plugin{
{
JSONData: plugins.JSONData{
ID: "test-app",
Type: "app",
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Test App",
Version: "1.0.0",
Links: []plugins.InfoLink{},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
GrafanaDependency: ">=8.0.0",
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{},
Roles: []plugins.RoleRegistration{
{
Role: plugins.Role{
Name: "plugins.app:test-app:reader",
DisplayName: "test-app reader",
Description: "View everything in the test-app plugin",
Permissions: []plugins.Permission{
{Action: "plugins.app:access", Scope: "plugins.app:id:test-app"},
{Action: "test-app.resource:read", Scope: "resources:*"},
{Action: "test-app.otherresource:toggle"},
},
},
Grants: []string{"Admin"},
},
},
Backend: false,
},
PluginDir: pluginDir,
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.PrivateSignature,
SignatureOrg: "gabrielmabille",
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
},
},
},
}
for _, tt := range tests {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000"
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
l := newLoader(tt.cfg, func(l *Loader) {
l.pluginRegistry = reg
l.pluginStorage = storage
l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths)
require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
}
pluginErrs := l.PluginErrors()
require.Len(t, pluginErrs, 0)
verifyState(t, tt.want, reg, procPrvdr, storage, procMgr)
}
}
func TestLoader_Load_Signature_RootURL(t *testing.T) {
const defaultAppURL = "http://localhost:3000/grafana"
@@ -1217,7 +1322,8 @@ func Test_setPathsBasedOnApp(t *testing.T) {
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage())
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage(),
fakes.NewFakeRoleRegistry())
for _, cb := range cbs {
cb(l)
@@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
@@ -108,7 +109,8 @@ func TestIntegrationPluginManager(t *testing.T) {
pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg)
reg := registry.ProvideService()
l := loader.ProvideService(pCfg, &licensing.OSSLicensingService{Cfg: cfg}, signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry))
l := loader.ProvideService(pCfg, &licensing.OSSLicensingService{Cfg: cfg}, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry())
ps, err := store.ProvideService(cfg, pCfg, reg, l)
require.NoError(t, err)
@@ -0,0 +1,31 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "private",
"signedByOrg": "gabrielmabille",
"signedByOrgName": "gabrielmabille",
"rootUrls": [
"http://localhost:3000/"
],
"plugin": "test-app",
"version": "1.0.0",
"time": 1666953431573,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "8017d19868809409e54e70eab116366de263005aa70960d44a12dc4dc5582cee"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.10
Comment: https://openpgpjs.org
wrgEARMKAAYFAmNbsNcAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
cIhm5z2+AgYqtKZ4tU/VBo8kOI49LfV85JKunAxPOvfaU3pRseRnWSyRBS0X
pKI2ekKebOSRZIs+zDPA0qTl1ihOY9bKe52pwwIJAf1IDq1P7G861dFilTuF
jCHQq6aS3NGy5o1N480Xof8PZdrI/xYDqSoy2F+688FR76ShyAM4B00Skt7c
9YSCsLx+
=cVti
-----END PGP SIGNATURE-----
@@ -0,0 +1,45 @@
{
"type": "app",
"name": "Test App",
"id": "test-app",
"info": {
"description": "Test App",
"author": {
"name": "Test Inc.",
"url": "http://test.com"
},
"keywords": ["test"],
"links": [],
"version": "1.0.0",
"updated": "2015-02-10"
},
"includes": [],
"roles": [
{
"role": {
"name": "plugins.app:test-app:reader",
"displayName": "test-app reader",
"description": "View everything in the test-app plugin",
"permissions": [
{
"action": "plugins.app:access",
"scope": "plugins.app:id:test-app"
},
{
"action": "test-app.resource:read",
"scope": "resources:*"
},
{
"action": "test-app.otherresource:toggle"
}
]
},
"grants": [
"Admin"
]
}
],
"dependencies": {
"grafanaDependency": ">=8.0.0"
}
}
+22
View File
@@ -266,3 +266,25 @@ type PreloadPlugin struct {
Path string `json:"path"`
Version string `json:"version"`
}
// Access-Control related definitions
// RoleRegistration stores a role and its assignments to basic roles
// (Viewer, Editor, Admin, Grafana Admin)
type RoleRegistration struct {
Role Role `json:"role"`
Grants []string `json:"grants"`
}
// Role is the model for Role in RBAC.
type Role struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Permissions []Permission `json:"permissions"`
}
type Permission struct {
Action string `json:"action"`
Scope string `json:"scope"`
}
+3
View File
@@ -80,6 +80,9 @@ func TestParseTreeTestdata(t *testing.T) {
rootid: "test-app",
skip: "has a 'page'-type include which isn't a known part of spec",
},
"test-app-with-roles": {
rootid: "test-app",
},
"unsigned-datasource": {
rootid: "test-datasource",
subpath: "plugin",
+3
View File
@@ -122,6 +122,9 @@ type JSONData struct {
Backend bool `json:"backend"`
Routes []*Route `json:"routes"`
// AccessControl settings
Roles []RoleRegistration `json:"roles,omitempty"`
// Panel settings
SkipDataQuery bool `json:"skipDataQuery"`