mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 20:52:34 +08:00
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:
@ -399,7 +399,7 @@ func setupHTTPServerWithCfgDb(
|
|||||||
userSvc = userMock
|
userSvc = userMock
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService())
|
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), featuremgmt.WithFeatures())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
ac = acimpl.ProvideAccessControl(cfg)
|
ac = acimpl.ProvideAccessControl(cfg)
|
||||||
userSvc = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService())
|
userSvc = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService())
|
||||||
|
@ -131,6 +131,51 @@ seqs: [
|
|||||||
// in all orgs
|
// in all orgs
|
||||||
autoEnabled?: bool
|
autoEnabled?: bool
|
||||||
|
|
||||||
|
// Optional list of RBAC RoleRegistrations.
|
||||||
|
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
|
||||||
|
// which characterizes what viewers, editors, admins, or grafana admins can do on the plugin.
|
||||||
|
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||||
|
// inherits them from the Viewer basic role.
|
||||||
|
roles?: [...#RoleRegistration]
|
||||||
|
|
||||||
|
// RoleRegistration describes an RBAC role and its assignments to basic roles.
|
||||||
|
// It organizes related RBAC permissions on the plugin into a role and defines which basic roles
|
||||||
|
// will get them by default.
|
||||||
|
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin
|
||||||
|
// which will be granted to Admins by default.
|
||||||
|
#RoleRegistration: {
|
||||||
|
// RBAC role definition to bundle related RBAC permissions on the plugin.
|
||||||
|
role: #Role
|
||||||
|
|
||||||
|
// Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin)
|
||||||
|
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||||
|
// inherits them from the Viewer basic role.
|
||||||
|
grants: [...#BasicRole]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
|
||||||
|
// each of which has an action and an optional scope.
|
||||||
|
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
|
||||||
|
#Role: {
|
||||||
|
name: string,
|
||||||
|
displayName: string,
|
||||||
|
description: string,
|
||||||
|
permissions: [...#Permission]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission describes an RBAC permission on the plugin. A permission has an action and an option
|
||||||
|
// scope.
|
||||||
|
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
|
||||||
|
#Permission: {
|
||||||
|
action: string,
|
||||||
|
scope?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'.
|
||||||
|
// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which
|
||||||
|
// in turn inherits them from the Viewer basic role.
|
||||||
|
#BasicRole: "Grafana Admin" | "Admin" | "Editor" | "Viewer"
|
||||||
|
|
||||||
// Dependencies needed by the plugin.
|
// Dependencies needed by the plugin.
|
||||||
dependencies: #Dependencies
|
dependencies: #Dependencies
|
||||||
|
|
||||||
|
@ -49,6 +49,17 @@ const (
|
|||||||
TypeSecretsmanager Type = "secretsmanager"
|
TypeSecretsmanager Type = "secretsmanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for BasicRole.
|
||||||
|
const (
|
||||||
|
BasicRoleAdmin BasicRole = "Admin"
|
||||||
|
|
||||||
|
BasicRoleEditor BasicRole = "Editor"
|
||||||
|
|
||||||
|
BasicRoleGrafanaAdmin BasicRole = "Grafana Admin"
|
||||||
|
|
||||||
|
BasicRoleViewer BasicRole = "Viewer"
|
||||||
|
)
|
||||||
|
|
||||||
// Defines values for DependencyType.
|
// Defines values for DependencyType.
|
||||||
const (
|
const (
|
||||||
DependencyTypeApp DependencyType = "app"
|
DependencyTypeApp DependencyType = "app"
|
||||||
@ -95,6 +106,17 @@ const (
|
|||||||
ReleaseStateStable ReleaseState = "stable"
|
ReleaseStateStable ReleaseState = "stable"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for RoleRegistrationGrants.
|
||||||
|
const (
|
||||||
|
RoleRegistrationGrantsAdmin RoleRegistrationGrants = "Admin"
|
||||||
|
|
||||||
|
RoleRegistrationGrantsEditor RoleRegistrationGrants = "Editor"
|
||||||
|
|
||||||
|
RoleRegistrationGrantsGrafanaAdmin RoleRegistrationGrants = "Grafana Admin"
|
||||||
|
|
||||||
|
RoleRegistrationGrantsViewer RoleRegistrationGrants = "Viewer"
|
||||||
|
)
|
||||||
|
|
||||||
// Model is the Go representation of a pluginmeta.
|
// Model is the Go representation of a pluginmeta.
|
||||||
//
|
//
|
||||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
@ -255,6 +277,13 @@ type Model struct {
|
|||||||
MinInterval *bool `json:"minInterval,omitempty"`
|
MinInterval *bool `json:"minInterval,omitempty"`
|
||||||
} `json:"queryOptions,omitempty"`
|
} `json:"queryOptions,omitempty"`
|
||||||
|
|
||||||
|
// Optional list of RBAC RoleRegistrations.
|
||||||
|
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
|
||||||
|
// which characterizes what viewers, editors, admins, or grafana admins can do on the plugin.
|
||||||
|
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||||
|
// inherits them from the Viewer basic role.
|
||||||
|
Roles *[]RoleRegistration `json:"roles,omitempty"`
|
||||||
|
|
||||||
// Routes is a list of proxy routes, if any. For datasource plugins only.
|
// Routes is a list of proxy routes, if any. For datasource plugins only.
|
||||||
Routes *[]Route `json:"routes,omitempty"`
|
Routes *[]Route `json:"routes,omitempty"`
|
||||||
|
|
||||||
@ -291,6 +320,14 @@ type Category string
|
|||||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
type Type string
|
type Type string
|
||||||
|
|
||||||
|
// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'.
|
||||||
|
// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which
|
||||||
|
// in turn inherits them from the Viewer basic role.
|
||||||
|
//
|
||||||
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
|
type BasicRole string
|
||||||
|
|
||||||
// BuildInfo is the Go representation of a pluginmeta.BuildInfo.
|
// BuildInfo is the Go representation of a pluginmeta.BuildInfo.
|
||||||
//
|
//
|
||||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
@ -475,12 +512,71 @@ type JWTTokenAuth struct {
|
|||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permission describes an RBAC permission on the plugin. A permission has an action and an option
|
||||||
|
// scope.
|
||||||
|
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
|
||||||
|
//
|
||||||
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
|
type Permission struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Scope *string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ReleaseState indicates release maturity state of a plugin.
|
// ReleaseState indicates release maturity state of a plugin.
|
||||||
//
|
//
|
||||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
type ReleaseState string
|
type ReleaseState string
|
||||||
|
|
||||||
|
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
|
||||||
|
// each of which has an action and an optional scope.
|
||||||
|
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
|
||||||
|
//
|
||||||
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
|
type Role struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Permissions []struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Scope *string `json:"scope,omitempty"`
|
||||||
|
} `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleRegistration describes an RBAC role and its assignments to basic roles.
|
||||||
|
// It organizes related RBAC permissions on the plugin into a role and defines which basic roles
|
||||||
|
// will get them by default.
|
||||||
|
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin
|
||||||
|
// which will be granted to Admins by default.
|
||||||
|
//
|
||||||
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
|
type RoleRegistration struct {
|
||||||
|
// Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin)
|
||||||
|
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||||
|
// inherits them from the Viewer basic role.
|
||||||
|
Grants []RoleRegistrationGrants `json:"grants"`
|
||||||
|
|
||||||
|
// RBAC role definition to bundle related RBAC permissions on the plugin.
|
||||||
|
Role struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Permissions []struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Scope *string `json:"scope,omitempty"`
|
||||||
|
} `json:"permissions"`
|
||||||
|
} `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleRegistrationGrants is the Go representation of a RoleRegistration.Grants.
|
||||||
|
//
|
||||||
|
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||||
|
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||||
|
type RoleRegistrationGrants string
|
||||||
|
|
||||||
// A proxy route used in datasource plugins for plugin authentication
|
// A proxy route used in datasource plugins for plugin authentication
|
||||||
// and adding headers to HTTP requests made by the plugin.
|
// and adding headers to HTTP requests made by the plugin.
|
||||||
// For more information, refer to [Authentication for data source
|
// For more information, refer to [Authentication for data source
|
||||||
|
@ -74,3 +74,8 @@ type PluginLoaderAuthorizer interface {
|
|||||||
// CanLoadPlugin confirms if a plugin is authorized to load
|
// CanLoadPlugin confirms if a plugin is authorized to load
|
||||||
CanLoadPlugin(plugin *Plugin) bool
|
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
|
||||||
|
}
|
||||||
|
@ -355,3 +355,15 @@ func (*FakeLicensingService) EnabledFeatures() map[string]bool {
|
|||||||
func (*FakeLicensingService) FeatureEnabled(_ string) bool {
|
func (*FakeLicensingService) FeatureEnabled(_ string) bool {
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
@ -42,6 +42,7 @@ type Loader struct {
|
|||||||
pluginFinder finder.Finder
|
pluginFinder finder.Finder
|
||||||
processManager process.Service
|
processManager process.Service
|
||||||
pluginRegistry registry.Service
|
pluginRegistry registry.Service
|
||||||
|
roleRegistry plugins.RoleRegistry
|
||||||
pluginInitializer initializer.Initializer
|
pluginInitializer initializer.Initializer
|
||||||
signatureValidator signature.Validator
|
signatureValidator signature.Validator
|
||||||
pluginStorage storage.Manager
|
pluginStorage storage.Manager
|
||||||
@ -51,14 +52,15 @@ type Loader struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
|
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),
|
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,
|
func New(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
|
||||||
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
|
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{
|
return &Loader{
|
||||||
pluginFinder: finder.New(),
|
pluginFinder: finder.New(),
|
||||||
pluginRegistry: pluginRegistry,
|
pluginRegistry: pluginRegistry,
|
||||||
@ -68,6 +70,7 @@ func New(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoa
|
|||||||
pluginStorage: pluginStorage,
|
pluginStorage: pluginStorage,
|
||||||
errs: make(map[string]*plugins.SignatureError),
|
errs: make(map[string]*plugins.SignatureError),
|
||||||
log: log.New("plugin.loader"),
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
|
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 {
|
for _, p := range verifiedPlugins {
|
||||||
|
@ -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) {
|
func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
||||||
const defaultAppURL = "http://localhost:3000/grafana"
|
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 {
|
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
|
||||||
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
|
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 {
|
for _, cb := range cbs {
|
||||||
cb(l)
|
cb(l)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/client"
|
"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/loader"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
@ -108,7 +109,8 @@ func TestIntegrationPluginManager(t *testing.T) {
|
|||||||
|
|
||||||
pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg)
|
pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg)
|
||||||
reg := registry.ProvideService()
|
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)
|
ps, err := store.ProvideService(cfg, pCfg, reg, l)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
31
pkg/plugins/manager/testdata/test-app-with-roles/MANIFEST.txt
vendored
Normal file
31
pkg/plugins/manager/testdata/test-app-with-roles/MANIFEST.txt
vendored
Normal file
@ -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-----
|
45
pkg/plugins/manager/testdata/test-app-with-roles/plugin.json
vendored
Normal file
45
pkg/plugins/manager/testdata/test-app-with-roles/plugin.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -266,3 +266,25 @@ type PreloadPlugin struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Version string `json:"version"`
|
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"`
|
||||||
|
}
|
||||||
|
@ -80,6 +80,9 @@ func TestParseTreeTestdata(t *testing.T) {
|
|||||||
rootid: "test-app",
|
rootid: "test-app",
|
||||||
skip: "has a 'page'-type include which isn't a known part of spec",
|
skip: "has a 'page'-type include which isn't a known part of spec",
|
||||||
},
|
},
|
||||||
|
"test-app-with-roles": {
|
||||||
|
rootid: "test-app",
|
||||||
|
},
|
||||||
"unsigned-datasource": {
|
"unsigned-datasource": {
|
||||||
rootid: "test-datasource",
|
rootid: "test-datasource",
|
||||||
subpath: "plugin",
|
subpath: "plugin",
|
||||||
|
@ -122,6 +122,9 @@ type JSONData struct {
|
|||||||
Backend bool `json:"backend"`
|
Backend bool `json:"backend"`
|
||||||
Routes []*Route `json:"routes"`
|
Routes []*Route `json:"routes"`
|
||||||
|
|
||||||
|
// AccessControl settings
|
||||||
|
Roles []RoleRegistration `json:"roles,omitempty"`
|
||||||
|
|
||||||
// Panel settings
|
// Panel settings
|
||||||
SkipDataQuery bool `json:"skipDataQuery"`
|
SkipDataQuery bool `json:"skipDataQuery"`
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ var wireExtsBasicSet = wire.NewSet(
|
|||||||
wire.Bind(new(setting.Provider), new(*setting.OSSImpl)),
|
wire.Bind(new(setting.Provider), new(*setting.OSSImpl)),
|
||||||
acimpl.ProvideService,
|
acimpl.ProvideService,
|
||||||
wire.Bind(new(accesscontrol.RoleRegistry), new(*acimpl.Service)),
|
wire.Bind(new(accesscontrol.RoleRegistry), new(*acimpl.Service)),
|
||||||
|
wire.Bind(new(plugins.RoleRegistry), new(*acimpl.Service)),
|
||||||
wire.Bind(new(accesscontrol.Service), new(*acimpl.Service)),
|
wire.Bind(new(accesscontrol.Service), new(*acimpl.Service)),
|
||||||
thumbs.ProvideCrawlerAuthSetupService,
|
thumbs.ProvideCrawlerAuthSetupService,
|
||||||
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
|
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
|
||||||
|
@ -12,20 +12,26 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ plugins.RoleRegistry = &Service{}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cacheTTL = 10 * time.Second
|
cacheTTL = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService) (*Service, error) {
|
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
||||||
service := ProvideOSSService(cfg, database.ProvideService(store), cache)
|
features *featuremgmt.FeatureManager) (*Service, error) {
|
||||||
|
service := ProvideOSSService(cfg, database.ProvideService(store), cache, features)
|
||||||
|
|
||||||
if !accesscontrol.IsDisabled(cfg) {
|
if !accesscontrol.IsDisabled(cfg) {
|
||||||
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
|
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
|
||||||
@ -37,13 +43,14 @@ func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRe
|
|||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService) *Service {
|
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features *featuremgmt.FeatureManager) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
store: store,
|
store: store,
|
||||||
log: log.New("accesscontrol.service"),
|
log: log.New("accesscontrol.service"),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||||
|
features: features,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@ -62,6 +69,7 @@ type Service struct {
|
|||||||
cache *localcache.CacheService
|
cache *localcache.CacheService
|
||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
roles map[string]*accesscontrol.RoleDTO
|
roles map[string]*accesscontrol.RoleDTO
|
||||||
|
features *featuremgmt.FeatureManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} {
|
func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} {
|
||||||
@ -198,3 +206,33 @@ func permissionCacheKey(user *user.SignedInUser) (string, error) {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("rbac-permissions-%s", key), nil
|
return fmt.Sprintf("rbac-permissions-%s", key), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(_ context.Context, ID, name string, regs []plugins.RoleRegistration) error {
|
||||||
|
// If accesscontrol is disabled no need to register roles
|
||||||
|
if accesscontrol.IsDisabled(s.cfg) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect behind feature toggle
|
||||||
|
if !s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
acRegs := pluginutils.ToRegistrations(name, regs)
|
||||||
|
for _, r := range acRegs {
|
||||||
|
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := accesscontrol.ValidateBuiltInRoles(r.Grants); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Debug("Registering plugin role", "role", r.Role.Name)
|
||||||
|
s.registrations.Append(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -12,8 +12,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@ -29,6 +31,7 @@ func setupTestEnv(t testing.TB) *Service {
|
|||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
store: database.ProvideService(db.InitTestDB(t)),
|
store: database.ProvideService(db.InitTestDB(t)),
|
||||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||||
|
features: featuremgmt.WithFeatures(),
|
||||||
}
|
}
|
||||||
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
||||||
return ac
|
return ac
|
||||||
@ -62,6 +65,7 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
db.InitTestDB(t),
|
db.InitTestDB(t),
|
||||||
routing.NewRouteRegister(),
|
routing.NewRouteRegister(),
|
||||||
localcache.ProvideService(),
|
localcache.ProvideService(),
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
require.NoError(t, errInitAc)
|
require.NoError(t, errInitAc)
|
||||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||||
@ -84,9 +88,7 @@ func TestService_DeclareFixedRoles(t *testing.T) {
|
|||||||
name: "should add registration",
|
name: "should add registration",
|
||||||
registrations: []accesscontrol.RoleRegistration{
|
registrations: []accesscontrol.RoleRegistration{
|
||||||
{
|
{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
|
||||||
Name: "fixed:test:test",
|
|
||||||
},
|
|
||||||
Grants: []string{"Admin"},
|
Grants: []string{"Admin"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,9 +98,7 @@ func TestService_DeclareFixedRoles(t *testing.T) {
|
|||||||
name: "should fail registration invalid role name",
|
name: "should fail registration invalid role name",
|
||||||
registrations: []accesscontrol.RoleRegistration{
|
registrations: []accesscontrol.RoleRegistration{
|
||||||
{
|
{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{Name: "custom:test:test"},
|
||||||
Name: "custom:test:test",
|
|
||||||
},
|
|
||||||
Grants: []string{"Admin"},
|
Grants: []string{"Admin"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -106,12 +106,10 @@ func TestService_DeclareFixedRoles(t *testing.T) {
|
|||||||
err: accesscontrol.ErrFixedRolePrefixMissing,
|
err: accesscontrol.ErrFixedRolePrefixMissing,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should fail registration invalid builtin role assignment",
|
name: "should fail registration invalid basic role assignment",
|
||||||
registrations: []accesscontrol.RoleRegistration{
|
registrations: []accesscontrol.RoleRegistration{
|
||||||
{
|
{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
|
||||||
Name: "fixed:test:test",
|
|
||||||
},
|
|
||||||
Grants: []string{"WrongAdmin"},
|
Grants: []string{"WrongAdmin"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -122,15 +120,11 @@ func TestService_DeclareFixedRoles(t *testing.T) {
|
|||||||
name: "should add multiple registrations at once",
|
name: "should add multiple registrations at once",
|
||||||
registrations: []accesscontrol.RoleRegistration{
|
registrations: []accesscontrol.RoleRegistration{
|
||||||
{
|
{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
|
||||||
Name: "fixed:test:test",
|
|
||||||
},
|
|
||||||
Grants: []string{"Admin"},
|
Grants: []string{"Admin"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{Name: "fixed:test2:test2"},
|
||||||
Name: "fixed:test2:test2",
|
|
||||||
},
|
|
||||||
Grants: []string{"Admin"},
|
Grants: []string{"Admin"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -164,6 +158,132 @@ func TestService_DeclareFixedRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_DeclarePluginRoles(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pluginID string
|
||||||
|
registrations []plugins.RoleRegistration
|
||||||
|
wantErr bool
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should work with empty list",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should add registration",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{Name: "plugins:test-app:test"},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should fail registration invalid role name",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{Name: "invalid.plugins:test-app:test"},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
err: &accesscontrol.ErrorInvalidRole{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should add registration with valid permissions",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{
|
||||||
|
Name: "plugins:test-app:test",
|
||||||
|
Permissions: []plugins.Permission{
|
||||||
|
{Action: "plugins.app:access"},
|
||||||
|
{Action: "test-app:read"},
|
||||||
|
{Action: "test-app.resource:read"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should fail registration invalid permission action",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{
|
||||||
|
Name: "plugins:test-app:test",
|
||||||
|
Permissions: []plugins.Permission{
|
||||||
|
{Action: "invalid.test-app.resource:read"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
err: &accesscontrol.ErrorInvalidRole{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should fail registration invalid basic role assignment",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{Name: "plugins:test-app:test"},
|
||||||
|
Grants: []string{"WrongAdmin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
err: accesscontrol.ErrInvalidBuiltinRole,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should add multiple registrations at once",
|
||||||
|
pluginID: "test-app",
|
||||||
|
registrations: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{Name: "plugins:test-app:test"},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: plugins.Role{Name: "plugins:test-app:test2"},
|
||||||
|
Grants: []string{"Admin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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{}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
err := ac.DeclarePluginRoles(context.Background(), tt.pluginID, tt.pluginID, tt.registrations)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, tt.err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
registrationCnt := 0
|
||||||
|
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||||
|
registrationCnt++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
assert.Equal(t, len(tt.registrations), registrationCnt,
|
||||||
|
"expected service registration list to contain all test registrations")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestService_RegisterFixedRoles(t *testing.T) {
|
func TestService_RegisterFixedRoles(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -210,6 +330,29 @@ func TestService_RegisterFixedRoles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "should register and assign fixed and plugins roles",
|
||||||
|
registrations: []accesscontrol.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: accesscontrol.RoleDTO{
|
||||||
|
Name: "plugins:test-app:test",
|
||||||
|
Permissions: []accesscontrol.Permission{{Action: "test-app:test"}},
|
||||||
|
},
|
||||||
|
Grants: []string{"Editor"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: accesscontrol.RoleDTO{
|
||||||
|
Name: "fixed:test2:test2",
|
||||||
|
Permissions: []accesscontrol.Permission{
|
||||||
|
{Action: "test:test2"},
|
||||||
|
{Action: "test:test3", Scope: "test:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Grants: []string{"Viewer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -1,10 +1,46 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFixedRolePrefixMissing = errors.New("fixed role should be prefixed with '" + FixedRolePrefix + "'")
|
ErrFixedRolePrefixMissing = errors.New("fixed role should be prefixed with '" + FixedRolePrefix + "'")
|
||||||
ErrInvalidBuiltinRole = errors.New("built-in role is not valid")
|
ErrInvalidBuiltinRole = errors.New("built-in role is not valid")
|
||||||
ErrInvalidScope = errors.New("invalid scope")
|
ErrInvalidScope = errors.New("invalid scope")
|
||||||
ErrResolverNotFound = errors.New("no resolver found")
|
ErrResolverNotFound = errors.New("no resolver found")
|
||||||
|
ErrPluginIDRequired = errors.New("plugin ID is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ErrorInvalidRole struct{}
|
||||||
|
|
||||||
|
func (e *ErrorInvalidRole) Error() string {
|
||||||
|
return "role is invalid"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorRolePrefixMissing struct {
|
||||||
|
Role string
|
||||||
|
Prefixes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorRolePrefixMissing) Error() string {
|
||||||
|
return fmt.Sprintf("expected role '%s' to be prefixed with any of '%v'", e.Role, e.Prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorRolePrefixMissing) Unwrap() error {
|
||||||
|
return &ErrorInvalidRole{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorActionPrefixMissing struct {
|
||||||
|
Action string
|
||||||
|
Prefixes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorActionPrefixMissing) Error() string {
|
||||||
|
return fmt.Sprintf("expected action '%s' to be prefixed with any of '%v'", e.Action, e.Prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorActionPrefixMissing) Unwrap() error {
|
||||||
|
return &ErrorInvalidRole{}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
@ -12,6 +13,7 @@ import (
|
|||||||
type fullAccessControl interface {
|
type fullAccessControl interface {
|
||||||
accesscontrol.AccessControl
|
accesscontrol.AccessControl
|
||||||
accesscontrol.Service
|
accesscontrol.Service
|
||||||
|
plugins.RoleRegistry
|
||||||
RegisterFixedRoles(context.Context) error
|
RegisterFixedRoles(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +22,7 @@ type Calls struct {
|
|||||||
GetUserPermissions []interface{}
|
GetUserPermissions []interface{}
|
||||||
IsDisabled []interface{}
|
IsDisabled []interface{}
|
||||||
DeclareFixedRoles []interface{}
|
DeclareFixedRoles []interface{}
|
||||||
|
DeclarePluginRoles []interface{}
|
||||||
GetUserBuiltInRoles []interface{}
|
GetUserBuiltInRoles []interface{}
|
||||||
RegisterFixedRoles []interface{}
|
RegisterFixedRoles []interface{}
|
||||||
RegisterAttributeScopeResolver []interface{}
|
RegisterAttributeScopeResolver []interface{}
|
||||||
@ -42,6 +45,7 @@ type Mock struct {
|
|||||||
GetUserPermissionsFunc func(context.Context, *user.SignedInUser, accesscontrol.Options) ([]accesscontrol.Permission, error)
|
GetUserPermissionsFunc func(context.Context, *user.SignedInUser, accesscontrol.Options) ([]accesscontrol.Permission, error)
|
||||||
IsDisabledFunc func() bool
|
IsDisabledFunc func() bool
|
||||||
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
||||||
|
DeclarePluginRolesFunc func(context.Context, string, string, []plugins.RoleRegistration) error
|
||||||
GetUserBuiltInRolesFunc func(user *user.SignedInUser) []string
|
GetUserBuiltInRolesFunc func(user *user.SignedInUser) []string
|
||||||
RegisterFixedRolesFunc func() error
|
RegisterFixedRolesFunc func() error
|
||||||
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
|
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
|
||||||
@ -169,6 +173,18 @@ func (m *Mock) RegisterFixedRoles(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their
|
||||||
|
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||||
|
// This mock returns no error unless an override is provided.
|
||||||
|
func (m *Mock) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error {
|
||||||
|
m.Calls.DeclarePluginRoles = append(m.Calls.DeclarePluginRoles, []interface{}{ctx, ID, name, regs})
|
||||||
|
// Use override if provided
|
||||||
|
if m.DeclarePluginRolesFunc != nil {
|
||||||
|
return m.DeclarePluginRolesFunc(ctx, ID, name, regs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Mock) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
func (m *Mock) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||||
m.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver)
|
m.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver)
|
||||||
m.Calls.RegisterAttributeScopeResolver = append(m.Calls.RegisterAttributeScopeResolver, []struct{}{})
|
m.Calls.RegisterAttributeScopeResolver = append(m.Calls.RegisterAttributeScopeResolver, []struct{}{})
|
||||||
|
@ -126,6 +126,10 @@ func (r *RoleDTO) IsFixed() bool {
|
|||||||
return strings.HasPrefix(r.Name, FixedRolePrefix)
|
return strings.HasPrefix(r.Name, FixedRolePrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RoleDTO) IsPlugin() bool {
|
||||||
|
return strings.HasPrefix(r.Name, PluginRolePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RoleDTO) IsBasic() bool {
|
func (r *RoleDTO) IsBasic() bool {
|
||||||
return strings.HasPrefix(r.Name, BasicRolePrefix) || strings.HasPrefix(r.UID, BasicRoleUIDPrefix)
|
return strings.HasPrefix(r.Name, BasicRolePrefix) || strings.HasPrefix(r.UID, BasicRoleUIDPrefix)
|
||||||
}
|
}
|
||||||
@ -273,6 +277,7 @@ const (
|
|||||||
FixedRolePrefix = "fixed:"
|
FixedRolePrefix = "fixed:"
|
||||||
ManagedRolePrefix = "managed:"
|
ManagedRolePrefix = "managed:"
|
||||||
BasicRolePrefix = "basic:"
|
BasicRolePrefix = "basic:"
|
||||||
|
PluginRolePrefix = "plugins:"
|
||||||
BasicRoleUIDPrefix = "basic_"
|
BasicRoleUIDPrefix = "basic_"
|
||||||
RoleGrafanaAdmin = "Grafana Admin"
|
RoleGrafanaAdmin = "Grafana Admin"
|
||||||
|
|
||||||
|
62
pkg/services/accesscontrol/pluginutils/utils.go
Normal file
62
pkg/services/accesscontrol/pluginutils/utils.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package pluginutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidatePluginPermissions errors when a permission does not match expected pattern for plugins
|
||||||
|
func ValidatePluginPermissions(pluginID string, permissions []ac.Permission) error {
|
||||||
|
for i := range permissions {
|
||||||
|
if permissions[i].Action != plugins.ActionAppAccess &&
|
||||||
|
!strings.HasPrefix(permissions[i].Action, pluginID+":") &&
|
||||||
|
!strings.HasPrefix(permissions[i].Action, pluginID+".") {
|
||||||
|
return &ac.ErrorActionPrefixMissing{Action: permissions[i].Action,
|
||||||
|
Prefixes: []string{plugins.ActionAppAccess, pluginID + ":", pluginID + "."}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePluginRole errors when a plugin role does not match expected pattern
|
||||||
|
// or doesn't have permissions matching the expected pattern.
|
||||||
|
func ValidatePluginRole(pluginID string, role ac.RoleDTO) error {
|
||||||
|
if pluginID == "" {
|
||||||
|
return ac.ErrPluginIDRequired
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(role.Name, ac.PluginRolePrefix+pluginID+":") {
|
||||||
|
return &ac.ErrorRolePrefixMissing{Role: role.Name, Prefixes: []string{ac.PluginRolePrefix + pluginID + ":"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidatePluginPermissions(pluginID, role.Permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRegistrations(pluginName string, regs []plugins.RoleRegistration) []ac.RoleRegistration {
|
||||||
|
res := make([]ac.RoleRegistration, 0, len(regs))
|
||||||
|
for i := range regs {
|
||||||
|
res = append(res, ac.RoleRegistration{
|
||||||
|
Role: ac.RoleDTO{
|
||||||
|
Version: 1,
|
||||||
|
Name: regs[i].Role.Name,
|
||||||
|
DisplayName: regs[i].Role.DisplayName,
|
||||||
|
Description: regs[i].Role.Description,
|
||||||
|
Group: pluginName,
|
||||||
|
Permissions: toPermissions(regs[i].Role.Permissions),
|
||||||
|
OrgID: ac.GlobalOrgID,
|
||||||
|
},
|
||||||
|
Grants: regs[i].Grants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPermissions(perms []plugins.Permission) []ac.Permission {
|
||||||
|
res := make([]ac.Permission, 0, len(perms))
|
||||||
|
for i := range perms {
|
||||||
|
res = append(res, ac.Permission{Action: perms[i].Action, Scope: perms[i].Scope})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
142
pkg/services/accesscontrol/pluginutils/utils_test.go
Normal file
142
pkg/services/accesscontrol/pluginutils/utils_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package pluginutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToRegistrations(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regs []plugins.RoleRegistration
|
||||||
|
want []ac.RoleRegistration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no registration",
|
||||||
|
regs: nil,
|
||||||
|
want: []ac.RoleRegistration{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registration gets converted successfully",
|
||||||
|
regs: []plugins.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: plugins.Role{
|
||||||
|
Name: "test:name",
|
||||||
|
DisplayName: "Test",
|
||||||
|
Description: "Test",
|
||||||
|
Permissions: []plugins.Permission{
|
||||||
|
{Action: "test:action"},
|
||||||
|
{Action: "test:action", Scope: "test:scope"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Grants: []string{"Admin", "Editor"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: plugins.Role{
|
||||||
|
Name: "test:name",
|
||||||
|
Permissions: []plugins.Permission{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []ac.RoleRegistration{
|
||||||
|
{
|
||||||
|
Role: ac.RoleDTO{
|
||||||
|
Version: 1,
|
||||||
|
Name: "test:name",
|
||||||
|
DisplayName: "Test",
|
||||||
|
Description: "Test",
|
||||||
|
Group: "PluginName",
|
||||||
|
Permissions: []ac.Permission{
|
||||||
|
{Action: "test:action"},
|
||||||
|
{Action: "test:action", Scope: "test:scope"},
|
||||||
|
},
|
||||||
|
OrgID: ac.GlobalOrgID,
|
||||||
|
},
|
||||||
|
Grants: []string{"Admin", "Editor"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: ac.RoleDTO{
|
||||||
|
Version: 1,
|
||||||
|
Name: "test:name",
|
||||||
|
Group: "PluginName",
|
||||||
|
Permissions: []ac.Permission{},
|
||||||
|
OrgID: ac.GlobalOrgID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := ToRegistrations("PluginName", tt.regs)
|
||||||
|
require.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePluginRole(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pluginID string
|
||||||
|
role ac.RoleDTO
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
pluginID: "",
|
||||||
|
role: ac.RoleDTO{Name: "plugins::"},
|
||||||
|
wantErr: ac.ErrPluginIDRequired,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid name",
|
||||||
|
pluginID: "test-app",
|
||||||
|
role: ac.RoleDTO{Name: "test-app:reader"},
|
||||||
|
wantErr: &ac.ErrorInvalidRole{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid id in name",
|
||||||
|
pluginID: "test-app",
|
||||||
|
role: ac.RoleDTO{Name: "plugins:test-app2:reader"},
|
||||||
|
wantErr: &ac.ErrorInvalidRole{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid name",
|
||||||
|
pluginID: "test-app",
|
||||||
|
role: ac.RoleDTO{Name: "plugins:test-app:reader"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid permission",
|
||||||
|
pluginID: "test-app",
|
||||||
|
role: ac.RoleDTO{
|
||||||
|
Name: "plugins:test-app:reader",
|
||||||
|
Permissions: []ac.Permission{{Action: "invalidtest-app:read"}},
|
||||||
|
},
|
||||||
|
wantErr: &ac.ErrorInvalidRole{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid permissions",
|
||||||
|
pluginID: "test-app",
|
||||||
|
role: ac.RoleDTO{
|
||||||
|
Name: "plugins:test-app:reader",
|
||||||
|
Permissions: []ac.Permission{
|
||||||
|
{Action: "plugins.app:access"},
|
||||||
|
{Action: "test-app:read"},
|
||||||
|
{Action: "test-app.resources:read"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidatePluginRole(tt.pluginID, tt.role)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -44,7 +44,7 @@ type Resolvers struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
|
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
|
||||||
s.log.Debug("adding scope attribute resolver for '%v'", prefix)
|
s.log.Debug("adding scope attribute resolver", "prefix", prefix)
|
||||||
s.attributeResolvers[prefix] = resolver
|
s.attributeResolvers[prefix] = resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user