diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 6dc1b9315e2..deb7e196dc7 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" @@ -19,27 +18,14 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" pluginClient "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/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" - "github.com/grafana/grafana/pkg/plugins/manager/process" - "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" - "github.com/grafana/grafana/pkg/plugins/manager/sources" - "github.com/grafana/grafana/pkg/plugins/manager/store" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/caching" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" - "github.com/grafana/grafana/pkg/services/pluginsintegration/config" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -66,36 +52,18 @@ func TestCallResource(t *testing.T) { coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil, nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil) - pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) - require.NoError(t, err) - reg := registry.ProvideService() - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) - proc := process.NewManager(reg) - discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) - bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) - initialize := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()) - terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) - require.NoError(t, err) + textCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry) - l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), - reg, fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate) - srcs := sources.ProvideService(cfg, pCfg) - ps, err := store.ProvideService(reg, srcs, l) - require.NoError(t, err) - - pcp := plugincontext.ProvideService(localcache.ProvideService(), ps, &datasources.FakeDataSourceService{}, + pcp := plugincontext.ProvideService(localcache.ProvideService(), textCtx.PluginStore, &datasources.FakeDataSourceService{}, pluginSettings.ProvideService(db.InitTestDB(t), fakeSecrets.NewFakeSecretsService())) srv := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = cfg hs.pluginContextProvider = pcp hs.QuotaService = quotatest.New(false, nil) - hs.pluginStore = ps - hs.pluginClient = pluginClient.ProvideService(reg, pCfg) + hs.pluginStore = textCtx.PluginStore + hs.pluginClient = textCtx.PluginClient hs.log = log.New("test") }) @@ -133,7 +101,7 @@ func TestCallResource(t *testing.T) { hs.Cfg = cfg hs.pluginContextProvider = pcp hs.QuotaService = quotatest.New(false, nil) - hs.pluginStore = ps + hs.pluginStore = textCtx.PluginStore hs.pluginClient = pc hs.log = log.New("test") }) diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 3a2958bc0ec..39ec9b98686 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -486,6 +486,17 @@ func (f *FakeBootstrapper) Bootstrap(ctx context.Context, src plugins.PluginSour return []*plugins.Plugin{}, nil } +type FakeValidator struct { + ValidateFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) +} + +func (f *FakeValidator) Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + if f.ValidateFunc != nil { + return f.ValidateFunc(ctx, ps) + } + return []*plugins.Plugin{}, nil +} + type FakeInitializer struct { IntializeFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go index 2f9299c9d3a..015cbd26dbc 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go @@ -73,6 +73,6 @@ func NewDefaultStaticDetectorsProvider() angulardetector.DetectorsProvider { // NewStaticInspector returns the default Inspector, which is a PatternsListInspector that only uses the // static (hardcoded) angular detection patterns. -func NewStaticInspector() (Inspector, error) { - return &PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()}, nil +func NewStaticInspector() Inspector { + return &PatternsListInspector{DetectorsProvider: NewDefaultStaticDetectorsProvider()} } diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 5fa6ac8e679..83992709e2e 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -2,154 +2,63 @@ package loader import ( "context" - "errors" - "time" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" - "github.com/grafana/grafana/pkg/plugins/manager/process" - "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/oauth" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" ) -var _ plugins.ErrorResolver = (*Loader)(nil) - type Loader struct { discovery discovery.Discoverer bootstrap bootstrap.Bootstrapper initializer initialization.Initializer termination termination.Terminator - - processManager process.Service - pluginRegistry registry.Service - roleRegistry plugins.RoleRegistry - signatureValidator signature.Validator - externalServiceRegistry oauth.ExternalServiceRegistry - assetPath *assetpath.Service - log log.Logger - cfg *config.Cfg - - angularInspector angularinspector.Inspector - - errs map[string]*plugins.SignatureError + validation validation.Validator + log log.Logger } -func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, - termination termination.Terminator) *Loader { - return New(cfg, authorizer, pluginRegistry, process.NewManager(pluginRegistry), roleRegistry, assetPath, - angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination) +func ProvideService(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, + initializer initialization.Initializer, termination termination.Terminator) *Loader { + return New(discovery, bootstrap, validation, initializer, termination) } -func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, - processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, - termination termination.Terminator) *Loader { +func New( + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, + initializer initialization.Initializer, termination termination.Terminator) *Loader { return &Loader{ - pluginRegistry: pluginRegistry, - signatureValidator: signature.NewValidator(authorizer), - processManager: processManager, - errs: make(map[string]*plugins.SignatureError), - log: log.New("plugin.loader"), - roleRegistry: roleRegistry, - cfg: cfg, - assetPath: assetPath, - angularInspector: angularInspector, - externalServiceRegistry: externalServiceRegistry, - discovery: discovery, - bootstrap: bootstrap, - initializer: initializer, - termination: termination, + discovery: discovery, + bootstrap: bootstrap, + validation: validation, + initializer: initializer, + termination: termination, + log: log.New("plugin.loader"), } } func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { - // discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err } - // - // bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins) if err != nil { return nil, err } - // - // - verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins)) - for _, plugin := range bootstrappedPlugins { - signingError := l.signatureValidator.Validate(plugin) - if signingError != nil { - l.log.Warn("Skipping loading plugin due to problem with signature", - "pluginID", plugin.ID, "status", signingError.SignatureStatus) - plugin.SignatureError = signingError - l.errs[plugin.ID] = signingError - // skip plugin so it will not be loaded any further - continue - } - - // clear plugin error if a pre-existing error has since been resolved - delete(l.errs, plugin.ID) - - // verify module.js exists for SystemJS to load. - // CDN plugins can be loaded with plugin.json only, so do not warn for those. - if !plugin.IsRenderer() && !plugin.IsCorePlugin() { - f, err := plugin.FS.Open("module.js") - if err != nil { - if errors.Is(err, plugins.ErrFileNotExist) { - l.log.Warn("Plugin missing module.js", "pluginID", plugin.ID, - "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.") - } - } else if f != nil { - if err := f.Close(); err != nil { - l.log.Warn("Could not close module.js", "pluginID", plugin.ID, "err", err) - } - } - } - - // detect angular for external plugins - if plugin.IsExternalPlugin() { - var err error - - cctx, canc := context.WithTimeout(ctx, time.Second*10) - plugin.AngularDetected, err = l.angularInspector.Inspect(cctx, plugin) - canc() - - if err != nil { - l.log.Warn("Could not inspect plugin for angular", "pluginID", plugin.ID, "err", err) - } - - // Do not initialize plugins if they're using Angular and Angular support is disabled - if plugin.AngularDetected && !l.cfg.AngularSupportEnabled { - l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", plugin.ID) - continue - } - } - - verifiedPlugins = append(verifiedPlugins, plugin) + verifiedPlugins, err := l.validation.Validate(ctx, bootstrappedPlugins) + if err != nil { + return nil, err } - // - // initializedPlugins, err := l.initializer.Initialize(ctx, verifiedPlugins) if err != nil { return nil, err } - // return initializedPlugins, nil } @@ -157,15 +66,3 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins func (l *Loader) Unload(ctx context.Context, pluginID string) error { return l.termination.Terminate(ctx, pluginID) } - -func (l *Loader) PluginErrors() []*plugins.Error { - errs := make([]*plugins.Error, 0, len(l.errs)) - for _, err := range l.errs { - errs = append(errs, &plugins.Error{ - PluginID: err.PluginID, - ErrorCode: err.AsErrorCode(), - }) - } - - return errs -} diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 992b25eeb6d..f8c675dcea0 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -14,15 +14,12 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" - "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/sources" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" ) @@ -61,12 +58,11 @@ func TestLoader_Load(t *testing.T) { return } tests := []struct { - name string - class plugins.Class - cfg *config.Cfg - pluginPaths []string - want []*plugins.Plugin - pluginErrors map[string]*plugins.Error + name string + class plugins.Class + cfg *config.Cfg + pluginPaths []string + want []*plugins.Plugin }{ { name: "Load a Core plugin", @@ -272,18 +268,13 @@ func TestLoader_Load(t *testing.T) { Signature: "unsigned", }, }, - }, { + }, + { name: "Load an unsigned plugin (production)", class: plugins.ClassExternal, cfg: &config.Cfg{}, pluginPaths: []string{"../testdata/unsigned-datasource"}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ - "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureMissing", - }, - }, }, { name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)", @@ -330,12 +321,6 @@ func TestLoader_Load(t *testing.T) { cfg: &config.Cfg{}, pluginPaths: []string{"../testdata/lacking-files"}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ - "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureInvalid", - }, - }, }, { name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvali", @@ -345,12 +330,6 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{"../testdata/lacking-files"}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ - "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureInvalid", - }, - }, }, { name: "Load a plugin with manifest which has a file not found in plugin folder", @@ -360,12 +339,6 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{"../testdata/invalid-v2-missing-file"}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ - "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureModified", - }, - }, }, { name: "Load a plugin with file which is missing from the manifest", @@ -375,12 +348,6 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{"../testdata/invalid-v2-extra-file"}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ - "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureModified", - }, - }, }, { name: "Load an app with includes", @@ -434,31 +401,19 @@ func TestLoader_Load(t *testing.T) { }, } for _, tt := range tests { - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) - - terminationStage, err := termination.New(tt.cfg, termination.Opts{}) - require.NoError(t, err) - - l := New(tt.cfg, signature.NewUnsignedAuthorizer(tt.cfg), fakes.NewFakePluginRegistry(), - fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{}, - discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), - initialization.New(tt.cfg, initialization.Opts{}), - terminationStage) - t.Run(tt.name, func(t *testing.T) { + terminationStage, err := termination.New(tt.cfg, termination.Opts{}) + require.NoError(t, err) + + l := New(discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), + validation.New(tt.cfg, validation.Opts{}), initialization.New(tt.cfg, initialization.Opts{}), + terminationStage) + got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, 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.Equal(t, len(tt.pluginErrors), len(pluginErrs)) - for _, pluginErr := range pluginErrs { - require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) - } }) } @@ -474,41 +429,41 @@ func TestLoader_Load(t *testing.T) { return plugins.Signature{}, false }, } - pJSON := plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource, Info: plugins.Info{Version: "1.0.0"}} - p := &plugins.Plugin{ - JSONData: pJSON, + pluginJSON := plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource, Info: plugins.Info{Version: "1.0.0"}} + plugin := &plugins.Plugin{ + JSONData: pluginJSON, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeCommunity, FS: plugins.NewFakeFS(), } - cfg := &config.Cfg{} - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) - var steps []string - l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), - fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), - angularInspector, &fakes.FakeOauthService{}, + l := New( &fakes.FakeDiscoverer{ DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { require.Equal(t, src, s) steps = append(steps, "discover") - return []*plugins.FoundBundle{{Primary: plugins.FoundPlugin{JSONData: pJSON}}}, nil + return []*plugins.FoundBundle{{Primary: plugins.FoundPlugin{JSONData: pluginJSON}}}, nil }, }, &fakes.FakeBootstrapper{ BootstrapFunc: func(ctx context.Context, s plugins.PluginSource, b []*plugins.FoundBundle) ([]*plugins.Plugin, error) { require.True(t, len(b) == 1) - require.Equal(t, b[0].Primary.JSONData, pJSON) + require.Equal(t, b[0].Primary.JSONData, pluginJSON) require.Equal(t, src, s) steps = append(steps, "bootstrap") - return []*plugins.Plugin{p}, nil + return []*plugins.Plugin{plugin}, nil }, - }, &fakes.FakeInitializer{ + }, &fakes.FakeValidator{ValidateFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + require.Equal(t, []*plugins.Plugin{plugin}, ps) + + steps = append(steps, "validate") + return ps, nil + }}, + &fakes.FakeInitializer{ IntializeFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { require.True(t, len(ps) == 1) - require.Equal(t, ps[0].JSONData, pJSON) + require.Equal(t, ps[0].JSONData, pluginJSON) steps = append(steps, "initialize") return ps, nil }, @@ -516,19 +471,14 @@ func TestLoader_Load(t *testing.T) { got, err := l.Load(context.Background(), src) require.NoError(t, err) - require.Equal(t, []*plugins.Plugin{p}, got) - require.Equal(t, []string{"discover", "bootstrap", "initialize"}, steps) - require.Zero(t, len(l.PluginErrors())) + require.Equal(t, []*plugins.Plugin{plugin}, got) + require.Equal(t, []string{"discover", "bootstrap", "validate", "initialize"}, steps) }) } func TestLoader_Unload(t *testing.T) { t.Run("Termination stage error is returned from Unload", func(t *testing.T) { pluginID := "grafana-test-panel" - cfg := &config.Cfg{} - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) - tcs := []struct { expectedErr error }{ @@ -541,9 +491,10 @@ func TestLoader_Unload(t *testing.T) { } for _, tc := range tcs { - l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), - fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), angularInspector, - &fakes.FakeOauthService{}, &fakes.FakeDiscoverer{}, &fakes.FakeBootstrapper{}, &fakes.FakeInitializer{}, + l := New(&fakes.FakeDiscoverer{}, + &fakes.FakeBootstrapper{}, + &fakes.FakeValidator{}, + &fakes.FakeInitializer{}, &fakes.FakeTerminator{ TerminateFunc: func(ctx context.Context, pID string) error { require.Equal(t, pluginID, pID) @@ -551,7 +502,7 @@ func TestLoader_Unload(t *testing.T) { }, }) - err = l.Unload(context.Background(), pluginID) + err := l.Unload(context.Background(), pluginID) require.ErrorIs(t, err, tc.expectedErr) } }) diff --git a/pkg/plugins/manager/pipeline/bootstrap/doc.go b/pkg/plugins/manager/pipeline/bootstrap/doc.go index 49f337bd072..9c4027d41da 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/doc.go +++ b/pkg/plugins/manager/pipeline/bootstrap/doc.go @@ -1,6 +1,5 @@ // Package bootstrap defines the Bootstrap stage of the plugin loader pipeline. // // The Bootstrap stage must implement the Bootstrapper interface. -// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) package bootstrap diff --git a/pkg/plugins/manager/pipeline/discovery/doc.go b/pkg/plugins/manager/pipeline/discovery/doc.go index 6db3e26f858..7d3dc9c4795 100644 --- a/pkg/plugins/manager/pipeline/discovery/doc.go +++ b/pkg/plugins/manager/pipeline/discovery/doc.go @@ -1,6 +1,5 @@ // Package discovery defines the Discovery stage of the plugin loader pipeline. // The Discovery stage must implement the Discoverer interface. -// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) package discovery diff --git a/pkg/plugins/manager/pipeline/doc.go b/pkg/plugins/manager/pipeline/doc.go index 8b32f3cb60d..6c3857f5ae9 100644 --- a/pkg/plugins/manager/pipeline/doc.go +++ b/pkg/plugins/manager/pipeline/doc.go @@ -4,7 +4,7 @@ // A plugin loader pipeline is defined by the following stages: // Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. // Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. -// Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) +// Validation: Validate the plugins based on some criteria (e.g. signature, angular, etc.) // Initialization: Initialize the plugin for use (e.g. register with Grafana, start the backend process, declare RBAC roles etc.) // - Termination: Terminate the plugin (e.g. stop the backend process, cleanup, etc.) diff --git a/pkg/plugins/manager/pipeline/initialization/doc.go b/pkg/plugins/manager/pipeline/initialization/doc.go index adf364c2c5a..eeea94866f9 100644 --- a/pkg/plugins/manager/pipeline/initialization/doc.go +++ b/pkg/plugins/manager/pipeline/initialization/doc.go @@ -1,6 +1,5 @@ // Package initialization defines the Initialization stage of the plugin loader pipeline. // // The Initialization stage must implement the Initializer interface. -// - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) package initialization diff --git a/pkg/plugins/manager/pipeline/termination/doc.go b/pkg/plugins/manager/pipeline/termination/doc.go index 4e4e934770c..9d674e3adbd 100644 --- a/pkg/plugins/manager/pipeline/termination/doc.go +++ b/pkg/plugins/manager/pipeline/termination/doc.go @@ -1,5 +1,4 @@ // Package termination defines the Termination stage of the plugin loader pipeline. // // The Termination stage must implement the Terminator interface. -// - Terminate(ctx context.Context, pluginID string) error package termination diff --git a/pkg/plugins/manager/pipeline/validation/doc.go b/pkg/plugins/manager/pipeline/validation/doc.go new file mode 100644 index 00000000000..8b69c1bebaf --- /dev/null +++ b/pkg/plugins/manager/pipeline/validation/doc.go @@ -0,0 +1,5 @@ +// Package validation defines the Validation stage of the plugin loader pipeline. +// +// The Validation stage must implement the Validator interface. + +package validation diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go new file mode 100644 index 00000000000..8f69fccc431 --- /dev/null +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -0,0 +1,113 @@ +package validation + +import ( + "context" + "errors" + "time" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" + "github.com/grafana/grafana/pkg/plugins/manager/signature" +) + +// DefaultValidateFuncs are the default ValidateFunc used for the Validate step of the Validation stage. +func DefaultValidateFuncs(cfg *config.Cfg) []ValidateFunc { + return []ValidateFunc{ + SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))), + ModuleJSValidationStep(), + AngularDetectionStep(cfg, angularinspector.NewStaticInspector()), + } +} + +type PluginSignatureValidator struct { + signatureValidator signature.Validator + log log.Logger +} + +func SignatureValidationStep(signatureValidator signature.Validator) ValidateFunc { + return newPluginSignatureValidator(signatureValidator).Validate +} + +func newPluginSignatureValidator(signatureValidator signature.Validator) *PluginSignatureValidator { + return &PluginSignatureValidator{ + signatureValidator: signatureValidator, + log: log.New("plugins.validator.signature"), + } +} + +func (v *PluginSignatureValidator) Validate(_ context.Context, p *plugins.Plugin) error { + return v.signatureValidator.ValidateSignature(p) +} + +type ModuleJSValidator struct { + log log.Logger +} + +func ModuleJSValidationStep() ValidateFunc { + return newModuleJSValidator().Validate +} + +func newModuleJSValidator() *ModuleJSValidator { + return &ModuleJSValidator{ + log: log.New("plugins.validator.module"), + } +} + +func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error { + if !p.IsRenderer() && !p.IsCorePlugin() { + f, err := p.FS.Open("module.js") + if err != nil { + if errors.Is(err, plugins.ErrFileNotExist) { + v.log.Warn("Plugin missing module.js", "pluginID", p.ID, + "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.") + } + } else if f != nil { + if err = f.Close(); err != nil { + v.log.Warn("Could not close module.js", "pluginID", p.ID, "err", err) + } + } + } + return nil +} + +type AngularDetector struct { + cfg *config.Cfg + angularInspector angularinspector.Inspector + log log.Logger +} + +func AngularDetectionStep(cfg *config.Cfg, angularInspector angularinspector.Inspector) ValidateFunc { + return newAngularDetector(cfg, angularInspector).Validate +} + +func newAngularDetector(cfg *config.Cfg, angularInspector angularinspector.Inspector) *AngularDetector { + return &AngularDetector{ + cfg: cfg, + angularInspector: angularInspector, + log: log.New("plugins.validator.angular"), + } +} + +func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error { + if p.IsExternalPlugin() { + var err error + + cctx, canc := context.WithTimeout(ctx, time.Second*10) + p.AngularDetected, err = a.angularInspector.Inspect(cctx, p) + canc() + + if err != nil { + a.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err) + } + + // Do not initialize plugins if they're using Angular and Angular support is disabled + if p.AngularDetected && !a.cfg.AngularSupportEnabled { + a.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", p.ID) + return errors.New("angular plugins are not supported") + } + } + + return nil +} diff --git a/pkg/plugins/manager/pipeline/validation/validation.go b/pkg/plugins/manager/pipeline/validation/validation.go new file mode 100644 index 00000000000..7c930c2e24f --- /dev/null +++ b/pkg/plugins/manager/pipeline/validation/validation.go @@ -0,0 +1,67 @@ +package validation + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Validator is responsible for the Validation stage of the plugin loader pipeline. +type Validator interface { + Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) +} + +// ValidateFunc is the function used for the Validate step of the Validation stage. +type ValidateFunc func(ctx context.Context, p *plugins.Plugin) error + +type Validate struct { + cfg *config.Cfg + validateSteps []ValidateFunc + log log.Logger +} + +type Opts struct { + ValidateFuncs []ValidateFunc +} + +// New returns a new Validation stage. +func New(cfg *config.Cfg, opts Opts) *Validate { + if opts.ValidateFuncs == nil { + opts.ValidateFuncs = DefaultValidateFuncs(cfg) + } + + return &Validate{ + cfg: cfg, + validateSteps: opts.ValidateFuncs, + log: log.New("plugins.validation"), + } +} + +// Validate will execute the Validate steps of the Validation stage. +func (t *Validate) Validate(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + if len(t.validateSteps) == 0 { + return ps, nil + } + + var err error + verifiedPlugins := make([]*plugins.Plugin, 0, len(ps)) + for _, p := range ps { + stepFailed := false + for _, validate := range t.validateSteps { + err = validate(ctx, p) + if err != nil && !errors.Is(err, nil) { + stepFailed = true + t.log.Error("Plugin verification failed", "pluginID", p.ID, "err", err) + break + } + } + if !stepFailed { + verifiedPlugins = append(verifiedPlugins, p) + } + } + + return verifiedPlugins, nil +} diff --git a/pkg/plugins/manager/signature/signature.go b/pkg/plugins/manager/signature/signature.go index 683828c33a3..9e7bf3e4733 100644 --- a/pkg/plugins/manager/signature/signature.go +++ b/pkg/plugins/manager/signature/signature.go @@ -5,19 +5,27 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" ) -type Validator struct { +type Validator interface { + ValidateSignature(plugin *plugins.Plugin) error +} + +type Validation struct { authorizer plugins.PluginLoaderAuthorizer log log.Logger } -func NewValidator(authorizer plugins.PluginLoaderAuthorizer) Validator { - return Validator{ +func ProvideValidatorService(authorizer plugins.PluginLoaderAuthorizer) *Validation { + return NewValidator(authorizer) +} + +func NewValidator(authorizer plugins.PluginLoaderAuthorizer) *Validation { + return &Validation{ authorizer: authorizer, log: log.New("plugin.signature.validator"), } } -func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { +func (s *Validation) ValidateSignature(plugin *plugins.Plugin) error { if plugin.Signature.IsValid() { s.log.Debug("Plugin has valid signature", "id", plugin.ID) return nil diff --git a/pkg/services/pluginsintegration/loader/fakes.go b/pkg/services/pluginsintegration/loader/fakes.go new file mode 100644 index 00000000000..fc636ea2e76 --- /dev/null +++ b/pkg/services/pluginsintegration/loader/fakes.go @@ -0,0 +1,38 @@ +package loader + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" +) + +type fakeSignatureErrorTracker struct { + RecordFunc func(ctx context.Context, err *plugins.SignatureError) + ClearFunc func(ctx context.Context, pluginID string) + SignatureErrorsFunc func(ctx context.Context) []*plugins.SignatureError +} + +func newFakeSignatureErrorTracker() *fakeSignatureErrorTracker { + return &fakeSignatureErrorTracker{} +} + +func (t *fakeSignatureErrorTracker) Record(ctx context.Context, err *plugins.SignatureError) { + if t.RecordFunc != nil { + t.RecordFunc(ctx, err) + return + } +} + +func (t *fakeSignatureErrorTracker) Clear(ctx context.Context, pluginID string) { + if t.ClearFunc != nil { + t.ClearFunc(ctx, pluginID) + return + } +} + +func (t *fakeSignatureErrorTracker) SignatureErrors(ctx context.Context) []*plugins.SignatureError { + if t.SignatureErrorsFunc != nil { + return t.SignatureErrorsFunc(ctx) + } + return nil +} diff --git a/pkg/services/pluginsintegration/loader/loader.go b/pkg/services/pluginsintegration/loader/loader.go index e75a6173292..d3f9260cd70 100644 --- a/pkg/services/pluginsintegration/loader/loader.go +++ b/pkg/services/pluginsintegration/loader/loader.go @@ -4,35 +4,25 @@ import ( "context" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" - "github.com/grafana/grafana/pkg/plugins/manager/process" - "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/oauth" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" ) -var _ plugins.ErrorResolver = (*Loader)(nil) var _ pluginsLoader.Service = (*Loader)(nil) type Loader struct { loader *pluginsLoader.Loader } -func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, processManager process.Service, - pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, - termination termination.Terminator, +func ProvideService(discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, validation validation.Validator, + initializer initialization.Initializer, termination termination.Terminator, ) *Loader { return &Loader{ - loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath, - angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination), + loader: pluginsLoader.New(discovery, bootstrap, validation, initializer, termination), } } @@ -43,7 +33,3 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins func (l *Loader) Unload(ctx context.Context, pluginID string) error { return l.loader.Unload(ctx, pluginID) } - -func (l *Loader) PluginErrors() []*plugins.Error { - return l.loader.PluginErrors() -} diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index d8a887f3a29..9ad0c471c6c 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -19,10 +19,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -30,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/setting" ) @@ -63,7 +60,7 @@ func TestLoader_Load(t *testing.T) { cfg *config.Cfg pluginPaths []string want []*plugins.Plugin - pluginErrors map[string]*plugins.Error + pluginErrors map[string]*plugins.SignatureError }{ { name: "Load a Core plugin", @@ -275,10 +272,10 @@ func TestLoader_Load(t *testing.T) { cfg: &config.Cfg{}, pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureMissing", + PluginID: "test-datasource", + SignatureStatus: plugins.SignatureStatusUnsigned, }, }, }, @@ -327,10 +324,10 @@ func TestLoader_Load(t *testing.T) { cfg: &config.Cfg{}, pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureInvalid", + PluginID: "test-datasource", + SignatureStatus: plugins.SignatureStatusInvalid, }, }, }, @@ -342,10 +339,10 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureInvalid", + PluginID: "test-datasource", + SignatureStatus: plugins.SignatureStatusInvalid, }, }, }, @@ -357,10 +354,10 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-missing-file")}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureModified", + PluginID: "test-datasource", + SignatureStatus: plugins.SignatureStatusModified, }, }, }, @@ -372,10 +369,10 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-extra-file")}, want: []*plugins.Plugin{}, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-datasource": { - PluginID: "test-datasource", - ErrorCode: "signatureModified", + PluginID: "test-datasource", + SignatureStatus: plugins.SignatureStatusModified, }, }, }, @@ -433,7 +430,8 @@ func TestLoader_Load(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + errTracker := pluginerrs.ProvideSignatureErrorTracker() + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker) t.Run(tt.name, func(t *testing.T) { got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) @@ -442,7 +440,7 @@ func TestLoader_Load(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) } - pluginErrs := l.PluginErrors() + pluginErrs := errTracker.SignatureErrors(context.Background()) require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) for _, pluginErr := range pluginErrs { require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) @@ -506,7 +504,7 @@ func TestLoader_Load_CustomSource(t *testing.T) { Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", }} - l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider()) + l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider(), newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassBundled @@ -536,7 +534,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { pluginPaths []string existingPlugins map[string]struct{} want []*plugins.Plugin - pluginErrors map[string]*plugins.Error + pluginErrors map[string]*plugins.SignatureError }{ { name: "Load multiple plugins (broken, valid, unsigned)", @@ -583,10 +581,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { SignatureOrg: "Will Browne", }, }, - pluginErrors: map[string]*plugins.Error{ + pluginErrors: map[string]*plugins.SignatureError{ "test-panel": { - PluginID: "test-panel", - ErrorCode: "signatureMissing", + PluginID: "test-panel", + SignatureStatus: plugins.SignatureStatusUnsigned, }, }, }, @@ -596,7 +594,9 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + errTracker := pluginerrs.ProvideSignatureErrorTracker() + + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker) t.Run(tt.name, func(t *testing.T) { got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -613,7 +613,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { if !cmp.Equal(got, tt.want, compareOpts...) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) } - pluginErrs := l.PluginErrors() + pluginErrs := errTracker.SignatureErrors(context.Background()) require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) for _, pluginErr := range pluginErrs { require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) @@ -696,7 +696,7 @@ func TestLoader_Load_RBACReady(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -711,8 +711,6 @@ func TestLoader_Load_RBACReady(t *testing.T) { 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, procMgr) } @@ -756,7 +754,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() cfg := &config.Cfg{GrafanaAppURL: defaultAppURL} - l := newLoader(t, cfg, reg, procMgr, procPrvdr) + l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -832,7 +830,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() cfg := &config.Cfg{} - l := newLoader(t, cfg, reg, procMgr, procPrvdr) + l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -921,7 +919,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { } procMgr := fakes.NewFakeProcessManager() cfg := &config.Cfg{} - l := newLoader(t, cfg, reg, procMgr, procPrvdr) + l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -1114,7 +1112,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { procMgr := fakes.NewFakeProcessManager() reg := fakes.NewFakePluginRegistry() cfg := &config.Cfg{} - l := newLoader(t, cfg, reg, procMgr, procPrvdr) + l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -1289,7 +1287,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() cfg := &config.Cfg{} - l := newLoader(t, cfg, reg, procMgr, procPrvdr) + l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -1314,34 +1312,37 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { } func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Service, - backendFactory plugins.BackendFactoryProvider) *Loader { + backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker) *Loader { assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) lic := fakes.NewFakeLicensingService() - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) + angularInspector := angularinspector.NewStaticInspector() terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) require.NoError(t, err) - return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), proc, reg, fakes.NewFakeRoleRegistry(), assets, - angularInspector, &fakes.FakeOauthService{}, - pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), + return ProvideService(pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), + pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker), pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()), terminate) } func newLoaderWithAngularInspector(t *testing.T, cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader { + assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) + lic := fakes.NewFakeLicensingService() reg := fakes.NewFakePluginRegistry() + backendFactory := fakes.NewFakeBackendProcessProvider() + proc := fakes.NewFakeProcessManager() - terminationStage, err := termination.New(cfg, termination.Opts{}) + terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) require.NoError(t, err) + sigErrTracker := pluginerrs.ProvideSignatureErrorTracker() - return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg, - fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), - angularInspector, &fakes.FakeOauthService{}, - discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}), - initialization.New(cfg, initialization.Opts{}), terminationStage) + return ProvideService(pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), + pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker), + pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()), + terminate) } func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 05f3660fdb3..e4dc2521f18 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -6,15 +6,19 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/oauth" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { @@ -37,6 +41,17 @@ func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *a }) } +func ProvideValidationStage(cfg *config.Cfg, sv signature.Validator, ai angularinspector.Inspector, + et pluginerrs.SignatureErrorTracker) *validation.Validate { + return validation.New(cfg, validation.Opts{ + ValidateFuncs: []validation.ValidateFunc{ + SignatureValidationStep(sv, et), + validation.ModuleJSValidationStep(), + validation.AngularDetectionStep(cfg, ai), + }, + }) +} + func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, bp plugins.BackendFactoryProvider, pm process.Service, externalServiceRegistry oauth.ExternalServiceRegistry, roleRegistry plugins.RoleRegistry) *initialization.Initialize { diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 7df828b5be9..0adf6f044e8 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -2,14 +2,18 @@ package pipeline import ( "context" + "errors" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" + "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) // ExternalServiceRegistration implements an InitializeFunc for registering external services. @@ -78,3 +82,42 @@ func ReportBuildMetrics(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, } return p, nil } + +// SignatureValidation implements a ValidateFunc for validating plugin signatures. +type SignatureValidation struct { + signatureValidator signature.Validator + errs pluginerrs.SignatureErrorTracker + log log.Logger +} + +// SignatureValidationStep returns a new ValidateFunc for validating plugin signatures. +func SignatureValidationStep(signatureValidator signature.Validator, + sigErr pluginerrs.SignatureErrorTracker) validation.ValidateFunc { + sv := &SignatureValidation{ + errs: sigErr, + signatureValidator: signatureValidator, + log: log.New("plugins.signature.validation"), + } + return sv.Validate +} + +// Validate validates the plugin signature. If a signature error is encountered, the error is recorded with the +// pluginerrs.SignatureErrorTracker. +func (v *SignatureValidation) Validate(ctx context.Context, p *plugins.Plugin) error { + err := v.signatureValidator.ValidateSignature(p) + if err != nil { + var sigErr *plugins.SignatureError + if errors.As(err, &sigErr) { + v.log.Warn("Skipping loading plugin due to problem with signature", + "pluginID", p.ID, "status", sigErr.SignatureStatus) + p.SignatureError = sigErr + v.errs.Record(ctx, sigErr) + } + return err + } + + // clear plugin error if a pre-existing error has since been resolved + v.errs.Clear(ctx, p.ID) + + return nil +} diff --git a/pkg/services/pluginsintegration/pluginerrs/errors.go b/pkg/services/pluginsintegration/pluginerrs/errors.go new file mode 100644 index 00000000000..47007d93938 --- /dev/null +++ b/pkg/services/pluginsintegration/pluginerrs/errors.go @@ -0,0 +1,71 @@ +package pluginerrs + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" +) + +var _ plugins.ErrorResolver = (*Store)(nil) + +type Store struct { + signatureErrs SignatureErrorTracker +} + +func ProvideStore(signatureErrs SignatureErrorTracker) *Store { + return &Store{ + signatureErrs: signatureErrs, + } +} + +func (s *Store) PluginErrors() []*plugins.Error { + sigErrs := s.signatureErrs.SignatureErrors(context.Background()) + errs := make([]*plugins.Error, 0, len(sigErrs)) + for _, err := range sigErrs { + errs = append(errs, &plugins.Error{ + PluginID: err.PluginID, + ErrorCode: err.AsErrorCode(), + }) + } + + return errs +} + +type SignatureErrorRegistry struct { + errs map[string]*plugins.SignatureError + log log.Logger +} + +type SignatureErrorTracker interface { + Record(ctx context.Context, err *plugins.SignatureError) + Clear(ctx context.Context, pluginID string) + SignatureErrors(ctx context.Context) []*plugins.SignatureError +} + +func ProvideSignatureErrorTracker() *SignatureErrorRegistry { + return newSignatureErrorRegistry() +} + +func newSignatureErrorRegistry() *SignatureErrorRegistry { + return &SignatureErrorRegistry{ + errs: make(map[string]*plugins.SignatureError), + log: log.New("plugins.errors"), + } +} + +func (r *SignatureErrorRegistry) Record(_ context.Context, signatureErr *plugins.SignatureError) { + r.errs[signatureErr.PluginID] = signatureErr +} + +func (r *SignatureErrorRegistry) Clear(_ context.Context, pluginID string) { + delete(r.errs, pluginID) +} + +func (r *SignatureErrorRegistry) SignatureErrors(_ context.Context) []*plugins.SignatureError { + errs := make([]*plugins.SignatureError, 0, len(r.errs)) + for _, err := range r.errs { + errs = append(errs, err) + } + return errs +} diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go similarity index 78% rename from pkg/plugins/manager/manager_integration_test.go rename to pkg/services/pluginsintegration/plugins_integration_test.go index 4dba9b74cef..53ff40329cf 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -1,4 +1,4 @@ -package manager +package pluginsintegration import ( "context" @@ -17,26 +17,10 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" - "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/angular/angularinspector" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" - "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" - "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/licensing" - "github.com/grafana/grafana/pkg/services/pluginsintegration/config" - plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -75,7 +59,7 @@ func TestIntegrationPluginManager(t *testing.T) { app_mode = production [plugin.test-app] - path=testdata/test-app + path=../../plugins/manager/testdata/test-app [plugin.test-panel] not=included @@ -113,37 +97,16 @@ func TestIntegrationPluginManager(t *testing.T) { graf := grafanads.ProvideService(sv2, nil) phlare := pyroscope.ProvideService(hcp, acimpl.ProvideAccessControl(cfg)) parca := parca.ProvideService(hcp) - coreRegistry := coreplugin.ProvideCoreRegistry(am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf, phlare, parca) - pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) - require.NoError(t, err) - reg := registry.ProvideService() - lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) - angularInspector, err := angularinspector.NewStaticInspector() - require.NoError(t, err) - proc := process.NewManager(reg) - - discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) - bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) - initialize := pipeline.ProvideInitializationStage(pCfg, reg, lic, provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()) - terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) - require.NoError(t, err) - - l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), - reg, fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate) - srcs := sources.ProvideService(cfg, pCfg) - ps, err := store.ProvideService(reg, srcs, l) - require.NoError(t, err) + testCtx := CreateIntegrationTestCtx(t, cfg, coreRegistry) ctx := context.Background() - verifyCorePluginCatalogue(t, ctx, ps) - verifyBundledPlugins(t, ctx, ps) - verifyPluginStaticRoutes(t, ctx, ps, reg) - verifyBackendProcesses(t, reg.Plugins(ctx)) - verifyPluginQuery(t, ctx, client.ProvideService(reg, pCfg)) + verifyCorePluginCatalogue(t, ctx, testCtx.PluginStore) + verifyBundledPlugins(t, ctx, testCtx.PluginStore) + verifyPluginStaticRoutes(t, ctx, testCtx.PluginStore, testCtx.PluginRegistry) + verifyBackendProcesses(t, testCtx.PluginRegistry.Plugins(ctx)) + verifyPluginQuery(t, ctx, testCtx.PluginClient) } func verifyPluginQuery(t *testing.T, ctx context.Context, c plugins.Client) { diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 720f749be41..16b50dbda9c 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -42,6 +43,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/loader" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/serviceregistration" @@ -72,15 +74,22 @@ var WireSet = wire.NewSet( wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)), pipeline.ProvideTerminationStage, wire.Bind(new(termination.Terminator), new(*termination.Terminate)), + pipeline.ProvideValidationStage, + wire.Bind(new(validation.Validator), new(*validation.Validate)), angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic, angularinspector.ProvideService, wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)), + signature.ProvideValidatorService, + wire.Bind(new(signature.Validator), new(*signature.Validation)), loader.ProvideService, wire.Bind(new(pluginLoader.Service), new(*loader.Loader)), - wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), + pluginerrs.ProvideSignatureErrorTracker, + wire.Bind(new(pluginerrs.SignatureErrorTracker), new(*pluginerrs.SignatureErrorRegistry)), + pluginerrs.ProvideStore, + wire.Bind(new(plugins.ErrorResolver), new(*pluginerrs.Store)), manager.ProvideInstaller, wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), registry.ProvideService, diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go new file mode 100644 index 00000000000..fdaf0218969 --- /dev/null +++ b/pkg/services/pluginsintegration/test_helper.go @@ -0,0 +1,114 @@ +package pluginsintegration + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" + "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" + pluginsCfg "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/loader/angular/angularinspector" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" + "github.com/grafana/grafana/pkg/plugins/manager/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" + "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/plugins/manager/store" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/config" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" + "github.com/grafana/grafana/pkg/setting" +) + +type IntegrationTestCtx struct { + PluginClient plugins.Client + PluginStore *store.Service + PluginRegistry registry.Service +} + +func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *coreplugin.Registry) *IntegrationTestCtx { + pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) + require.NoError(t, err) + + cdn := pluginscdn.ProvideService(pCfg) + reg := registry.ProvideService() + angularInspector := angularinspector.NewStaticInspector() + proc := process.NewManager(reg) + errTracker := pluginerrs.ProvideSignatureErrorTracker() + + disc := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(true), reg) + boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(cdn)) + valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector, errTracker) + init := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()) + term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) + require.NoError(t, err) + + l := CreateTestLoader(t, pCfg, LoaderOpts{ + Discoverer: disc, + Bootstrapper: boot, + Validator: valid, + Initializer: init, + Terminator: term, + }) + + ps, err := store.ProvideService(reg, sources.ProvideService(cfg, pCfg), l) + require.NoError(t, err) + + return &IntegrationTestCtx{ + PluginClient: client.ProvideService(reg, pCfg), + PluginStore: ps, + PluginRegistry: reg, + } +} + +type LoaderOpts struct { + Discoverer discovery.Discoverer + Bootstrapper bootstrap.Bootstrapper + Validator validation.Validator + Terminator termination.Terminator + Initializer initialization.Initializer +} + +func CreateTestLoader(t *testing.T, cfg *pluginsCfg.Cfg, opts LoaderOpts) *loader.Loader { + if opts.Discoverer == nil { + opts.Discoverer = pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(cfg.DevMode), registry.ProvideService()) + } + + if opts.Bootstrapper == nil { + opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(cfg))) + } + + if opts.Validator == nil { + opts.Validator = pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularinspector.NewStaticInspector(), pluginerrs.ProvideSignatureErrorTracker()) + } + + if opts.Initializer == nil { + reg := registry.ProvideService() + coreRegistry := coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc)) + opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), process.NewManager(reg), &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()) + } + + if opts.Terminator == nil { + var err error + reg := registry.ProvideService() + opts.Terminator, err = pipeline.ProvideTerminationStage(cfg, reg, process.NewManager(reg)) + require.NoError(t, err) + } + + return loader.New(opts.Discoverer, opts.Bootstrapper, opts.Validator, opts.Initializer, opts.Terminator) +}