diff --git a/apps/plugins/pkg/app/meta/local.go b/apps/plugins/pkg/app/meta/local.go index 2c699520cfc..af40316cd73 100644 --- a/apps/plugins/pkg/app/meta/local.go +++ b/apps/plugins/pkg/app/meta/local.go @@ -13,10 +13,9 @@ const ( ) // PluginAssetsCalculator is an interface for calculating plugin asset information. -// LocalProvider requires this to calculate loading strategy and module hash. +// LocalProvider requires this to calculate loading strategy. type PluginAssetsCalculator interface { LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy - ModuleHash(ctx context.Context, p pluginstore.Plugin) string } // LocalProvider retrieves plugin metadata for locally installed plugins. @@ -27,7 +26,7 @@ type LocalProvider struct { } // NewLocalProvider creates a new LocalProvider for locally installed plugins. -// pluginAssets is required for calculating loading strategy and module hash. +// pluginAssets is required for calculating loading strategy. func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider { return &LocalProvider{ store: pluginStore, @@ -43,7 +42,7 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) ( } loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin) - moduleHash := p.pluginAssets.ModuleHash(ctx, plugin) + moduleHash := plugin.ModuleHash spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash) return &Result{ diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index b57262087e5..64165cfb98a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -161,7 +161,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AliasIDs: panel.AliasIDs, Info: panel.Info, Module: panel.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel), + ModuleHash: panel.ModuleHash, BaseURL: panel.BaseURL, SkipDataQuery: panel.SkipDataQuery, Suggestions: panel.Suggestions, @@ -527,7 +527,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug JSONData: plugin.JSONData, Signature: plugin.Signature, Module: plugin.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), + ModuleHash: plugin.ModuleHash, BaseURL: plugin.BaseURL, Angular: plugin.Angular, MultiValueFilterOperators: plugin.MultiValueFilterOperators, @@ -641,7 +641,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin), Extensions: plugin.Extensions, Dependencies: plugin.Dependencies, - ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin), + ModuleHash: plugin.ModuleHash, Translations: plugin.Translations, BuildMode: plugin.BuildMode, } diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index ba67837be5d..4741f6b10bc 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -20,8 +20,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -79,8 +77,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F var pluginsAssets = passets if pluginsAssets == nil { - sig := signature.ProvideService(pluginsCfg, statickey.New()) - pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore) + pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, pluginStore) } hs := &HTTPServer{ @@ -714,6 +711,6 @@ func newPluginAssets() func() *pluginassets.Service { func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service { return func() *pluginassets.Service { - return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{}) + return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), &pluginstore.FakePluginStore{}) } } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 50e32326565..079dae066bb 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -201,7 +201,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response. Includes: plugin.Includes, BaseUrl: plugin.BaseURL, Module: plugin.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), + ModuleHash: plugin.ModuleHash, DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL), State: plugin.State, Signature: plugin.Signature, diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 342c6293d7e..238b0b3390f 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -28,8 +28,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "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/pluginerrs" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -848,8 +846,7 @@ func Test_PluginsSettings(t *testing.T) { } pCfg := &config.PluginManagementCfg{} pluginCDN := pluginscdn.ProvideService(pCfg) - sig := signature.ProvideService(pCfg, statickey.New()) - hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore) + hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, hs.pluginStore) hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker) hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( hs.Cfg, diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 9b719b8b180..1d33d34e6c9 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -140,7 +140,9 @@ type Licensing interface { } type SignatureCalculator interface { - Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error) + // Calculate calculates the signature and returns both the signature and the manifest. + // The manifest may be nil if the plugin is unsigned or if an error occurred. + Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, *PluginManifest, error) } type KeyStore interface { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 7613392f497..f4c63e71e15 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -216,10 +216,27 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1622547655175, + Files: map[string]string{ + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index f20c1ff1ead..c129e203dc5 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/tracing" "github.com/grafana/grafana/pkg/semconv" ) @@ -54,7 +55,7 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap { } if opts.DecorateFuncs == nil { - opts.DecorateFuncs = DefaultDecorateFuncs(cfg) + opts.DecorateFuncs = DefaultDecorateFuncs(cfg, pluginscdn.ProvideService(cfg)) } return &Bootstrap{ diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go index 5c365ebb47c..5ab85dc6e43 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) // DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. @@ -28,12 +29,13 @@ func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator p } // DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. -func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc { +func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) []DecorateFunc { return []DecorateFunc{ AppDefaultNavURLDecorateFunc, TemplateDecorateFunc, AppChildDecorateFunc(), SkipHostEnvVarsDecorateFunc(cfg), + ModuleHashDecorateFunc(cfg, cdn), } } @@ -48,19 +50,30 @@ func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator // Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error) { - sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + // Calculate signature and cache manifest + sig, manifest, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) if err != nil { c.log.Warn("Could not calculate plugin signature state", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } + plugin, err := c.pluginFactoryFunc(bundle, src.PluginClass(ctx), sig) if err != nil { c.log.Error("Could not create primary plugin base", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } + + plugin.Manifest = manifest + res := make([]*plugins.Plugin, 0, len(plugin.Children)+1) res = append(res, plugin) - res = append(res, plugin.Children...) + for _, child := range plugin.Children { + // Child plugins use the parent's manifest + if child.Parent != nil && child.Parent.Manifest != nil { + child.Manifest = child.Parent.Manifest + } + res = append(res, child) + } return res, nil } @@ -145,3 +158,11 @@ func SkipHostEnvVarsDecorateFunc(cfg *config.PluginManagementCfg) DecorateFunc { return p, nil } } + +// ModuleHashDecorateFunc returns a DecorateFunc that calculates and sets the module hash for the plugin. +func ModuleHashDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) DecorateFunc { + return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + p.ModuleHash = pluginassets.CalculateModuleHash(p, cfg, cdn) + return p, nil + } +} diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 6d790c79873..4b6bc531d89 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -14,7 +14,6 @@ import ( "path" "path/filepath" "runtime" - "strings" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/clearsign" @@ -37,26 +36,6 @@ var ( fromSlash = filepath.FromSlash ) -// PluginManifest holds details for the file manifest -type PluginManifest struct { - Plugin string `json:"plugin"` - Version string `json:"version"` - KeyID string `json:"keyId"` - Time int64 `json:"time"` - Files map[string]string `json:"files"` - - // V2 supported fields - ManifestVersion string `json:"manifestVersion"` - SignatureType plugins.SignatureType `json:"signatureType"` - SignedByOrg string `json:"signedByOrg"` - SignedByOrgName string `json:"signedByOrgName"` - RootURLs []string `json:"rootUrls"` -} - -func (m *PluginManifest) IsV2() bool { - return strings.HasPrefix(m.ManifestVersion, "2.") -} - type Signature struct { kr plugins.KeyRetriever cfg *config.PluginManagementCfg @@ -87,14 +66,14 @@ func DefaultCalculator(cfg *config.PluginManagementCfg) *Signature { // readPluginManifest attempts to read and verify the plugin manifest // if any error occurs or the manifest is not valid, this will return an error -func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) { +func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*plugins.PluginManifest, error) { block, _ := clearsign.Decode(body) if block == nil { return nil, errors.New("unable to decode manifest") } // Convert to a well typed object - var manifest PluginManifest + var manifest plugins.PluginManifest err := json.Unmarshal(block.Plaintext, &manifest) if err != nil { return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err) @@ -111,7 +90,7 @@ var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned") // ReadPluginManifestFromFS reads the plugin manifest from the provided plugins.FS. // If the manifest is not found, it will return an error wrapping ErrSignatureTypeUnsigned. -func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*PluginManifest, error) { +func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*plugins.PluginManifest, error) { f, err := pfs.Open("MANIFEST.txt") if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { @@ -140,9 +119,9 @@ func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS return manifest, nil } -func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) { +func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, *plugins.PluginManifest, error) { if defaultSignature, exists := src.DefaultSignature(ctx, plugin.JSONData.ID); exists { - return defaultSignature, nil + return defaultSignature, nil, nil } manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS) @@ -151,29 +130,29 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusUnsigned, - }, nil + }, nil, nil case err != nil: s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } if !manifest.IsV2() { return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } fsFiles, err := plugin.FS.Files() if err != nil { - return plugins.Signature{}, fmt.Errorf("files: %w", err) + return plugins.Signature{}, nil, fmt.Errorf("files: %w", err) } if len(fsFiles) == 0 { s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } // Make sure the versions all match @@ -181,20 +160,20 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid because ID or Version mismatch", "pluginId", plugin.JSONData.ID, "manifestPluginId", manifest.Plugin, "pluginVersion", plugin.JSONData.Info.Version, "manifestPluginVersion", manifest.Version) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } // Validate that plugin is running within defined root URLs if len(manifest.RootURLs) > 0 { if match, err := urlMatch(manifest.RootURLs, s.cfg.GrafanaAppURL, manifest.SignatureType); err != nil { s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) - return plugins.Signature{}, err + return plugins.Signature{}, nil, err } else if !match { s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, "appUrl", s.cfg.GrafanaAppURL, "rootUrls", manifest.RootURLs) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } } @@ -207,7 +186,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid", "pluginId", plugin.JSONData.ID, "error", err) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } manifestFiles[p] = struct{}{} @@ -236,7 +215,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID) @@ -244,7 +223,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu Status: plugins.SignatureStatusValid, Type: manifest.SignatureType, SigningOrg: manifest.SignedByOrgName, - }, nil + }, manifest, nil } func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error { @@ -321,7 +300,7 @@ func (r invalidFieldErr) Error() string { return fmt.Sprintf("valid manifest field %s is required", r.field) } -func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, block *clearsign.Block) error { +func (s *Signature) validateManifest(ctx context.Context, m plugins.PluginManifest, block *clearsign.Block) error { if len(m.Plugin) == 0 { return invalidFieldErr{field: "plugin"} } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index d399268b464..5768f0f32f5 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -164,7 +164,7 @@ func TestCalculate(t *testing.T) { for _, tc := range tcs { basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}) - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -192,7 +192,7 @@ func TestCalculate(t *testing.T) { runningWindows = true s := provideDefaultTestService() - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -260,7 +260,7 @@ func TestCalculate(t *testing.T) { require.NoError(t, err) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) require.NoError(t, err) - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -396,7 +396,7 @@ func TestFSPathSeparatorFiles(t *testing.T) { } } -func fileList(manifest *PluginManifest) []string { +func fileList(manifest *plugins.PluginManifest) []string { keys := make([]string, 0, len(manifest.Files)) for k := range manifest.Files { keys = append(keys, k) @@ -682,52 +682,52 @@ func Test_urlMatch_private(t *testing.T) { func Test_validateManifest(t *testing.T) { tcs := []struct { name string - manifest *PluginManifest + manifest *plugins.PluginManifest expectedErr string }{ { name: "Empty plugin field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Plugin = "" }), expectedErr: "valid manifest field plugin is required", }, { name: "Empty keyId field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.KeyID = "" }), expectedErr: "valid manifest field keyId is required", }, { name: "Empty signedByOrg field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrg = "" }), expectedErr: "valid manifest field signedByOrg is required", }, { name: "Empty signedByOrgName field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrgName = "" }), expectedErr: "valid manifest field SignedByOrgName is required", }, { name: "Empty signatureType field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Invalid signatureType field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "invalidSignatureType" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Empty files field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Files = map[string]string{} }), expectedErr: "valid manifest field files is required", }, { name: "Empty time field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Time = 0 }), expectedErr: "valid manifest field time is required", }, { name: "Empty version field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Version = "" }), expectedErr: "valid manifest field version is required", }, } @@ -740,10 +740,10 @@ func Test_validateManifest(t *testing.T) { } } -func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest { +func createV2Manifest(t *testing.T, cbs ...func(*plugins.PluginManifest)) *plugins.PluginManifest { t.Helper() - m := &PluginManifest{ + m := &plugins.PluginManifest{ Plugin: "grafana-test-app", Version: "2.5.3", KeyID: "7e4d0c6a708866e7", diff --git a/pkg/plugins/pluginassets/modulehash.go b/pkg/plugins/pluginassets/modulehash.go new file mode 100644 index 00000000000..fd1ff13c578 --- /dev/null +++ b/pkg/plugins/pluginassets/modulehash.go @@ -0,0 +1,90 @@ +package pluginassets + +import ( + "encoding/base64" + "encoding/hex" + "path" + "path/filepath" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +// CalculateModuleHash calculates the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. +// The module hash is read from the plugin's cached manifest. +// For nested plugins, the module hash is read from the root parent plugin's manifest. +// If the plugin is unsigned or not a CDN plugin, an empty string is returned. +func CalculateModuleHash(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) string { + if cfg == nil || !cfg.Features.SriChecksEnabled { + return "" + } + + if !p.Signature.IsValid() { + return "" + } + + rootParent := findRootParent(p) + if rootParent.Manifest == nil { + return "" + } + + if !rootParent.Manifest.IsV2() { + return "" + } + + if !cdnEnabled(rootParent, cdn) { + return "" + } + + modulePath := getModulePathInManifest(p, rootParent) + moduleHash, ok := rootParent.Manifest.Files[modulePath] + if !ok { + return "" + } + + return convertHashForSRI(moduleHash) +} + +// findRootParent returns the root parent plugin (the one that contains the manifest). +// For non-nested plugins, it returns the plugin itself. +func findRootParent(p *plugins.Plugin) *plugins.Plugin { + root := p + for root.Parent != nil { + root = root.Parent + } + return root +} + +// getModulePathInManifest returns the path to module.js as it appears in the manifest. +// For nested plugins, this is the relative path from the root parent to the plugin's module.js. +// For non-nested plugins, this is simply "module.js". +func getModulePathInManifest(p *plugins.Plugin, rootParent *plugins.Plugin) string { + if p == rootParent { + return "module.js" + } + + // Calculate the relative path from root parent to this plugin + relPath, err := rootParent.FS.Rel(p.FS.Base()) + if err != nil { + return "" + } + + // MANIFEST.txt uses forward slashes as path separators + pluginRootPath := filepath.ToSlash(relPath) + return path.Join(pluginRootPath, "module.js") +} + +// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. +func convertHashForSRI(h string) string { + hb, err := hex.DecodeString(h) + if err != nil { + return "" + } + return "sha256-" + base64.StdEncoding.EncodeToString(hb) +} + +// cdnEnabled checks if a plugin is loaded via CDN +func cdnEnabled(p *plugins.Plugin, cdn *pluginscdn.Service) bool { + return p.FS.Type().CDN() || cdn.PluginSupported(p.ID) +} diff --git a/pkg/plugins/pluginassets/modulehash_test.go b/pkg/plugins/pluginassets/modulehash_test.go new file mode 100644 index 00000000000..c11fcb7f6a9 --- /dev/null +++ b/pkg/plugins/pluginassets/modulehash_test.go @@ -0,0 +1,356 @@ +package pluginassets + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +func TestConvertHashForSRI(t *testing.T) { + for _, tc := range []struct { + hash string + expHash string + expErr bool + }{ + { + hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", + expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", + }, + { + hash: "not-a-valid-hash", + expErr: true, + }, + } { + t.Run(tc.hash, func(t *testing.T) { + r := convertHashForSRI(tc.hash) + if tc.expErr { + // convertHashForSRI returns empty string on error + require.Empty(t, r) + } else { + require.Equal(t, tc.expHash, r) + } + }) + } +} + +func TestCalculateModuleHash(t *testing.T) { + const ( + pluginID = "grafana-test-datasource" + parentPluginID = "grafana-test-app" + ) + + // Helper to create a plugin with manifest + createPluginWithManifest := func(id string, manifest *plugins.PluginManifest, parent *plugins.Plugin) *plugins.Plugin { + p := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: id, + }, + Signature: plugins.SignatureStatusValid, + Manifest: manifest, + } + if parent != nil { + p.Parent = parent + } + return p + } + + // Helper to create a v2 manifest + createV2Manifest := func(files map[string]string) *plugins.PluginManifest { + return &plugins.PluginManifest{ + ManifestVersion: "2.0.0", + Files: files, + } + } + + for _, tc := range []struct { + name string + plugin *plugins.Plugin + cfg *config.PluginManagementCfg + cdn *pluginscdn.Service + expModuleHash string + }{ + { + name: "should return empty string when cfg is nil", + plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ + "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + }), nil), + cfg: nil, + cdn: nil, + expModuleHash: "", + }, + { + name: "should return empty string when SRI checks are disabled", + plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ + "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + }), nil), + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: false}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string for unsigned plugin", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusUnsigned, + Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return module hash for valid plugin", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=", + }, + { + name: "should return empty string when manifest is nil", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string for v1 manifest", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: &plugins.PluginManifest{ + ManifestVersion: "1.0.0", + Files: map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}, + }, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string when module.js is not in manifest", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "missing module.js entry from MANIFEST.txt should not return module hash", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "signed status but missing MANIFEST.txt should not return module hash", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + // parentPluginID (/) + // └── pluginID (/datasource) + name: "nested plugin should return module hash from parent MANIFEST.txt", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-BNcNsJHZbEd1+zK6Wo+EzCKJPrQ6/bZJcmZh1EJcZxE=", + }, + { + // parentPluginID (/) + // └── pluginID (/panels/one) + name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", + }, + { + // grand-parent-app (/) + // ├── parent-datasource (/datasource) + // │ └── child-panel (/datasource/panels/one) + name: "nested plugin of a nested plugin should return module hash from grandparent MANIFEST.txt", + plugin: func() *plugins.Plugin { + grandparent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "grand-parent-app"}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + "datasource/panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested")), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "parent-datasource"}, + Signature: plugins.SignatureStatusValid, + Parent: grandparent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "child-panel"}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + "child-panel": {"cdn": "true"}, + "parent-datasource": {"cdn": "true"}, + "grand-parent-app": {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + "child-panel": {"cdn": "true"}, + "parent-datasource": {"cdn": "true"}, + "grand-parent-app": {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", + }, + { + name: "nested plugin should not return module hash when parent manifest is nil", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, // Parent has no manifest + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + result := CalculateModuleHash(tc.plugin, tc.cfg, tc.cdn) + require.Equal(t, tc.expModuleHash, result) + }) + } +} diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js b/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index bf1b23a35b0..e1c26b4f87d 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -40,6 +40,7 @@ type Plugin struct { Pinned bool // Signature fields + Manifest *PluginManifest Signature SignatureStatus SignatureType SignatureType SignatureOrg string @@ -48,8 +49,9 @@ type Plugin struct { Error *Error // SystemJS fields - Module string - BaseURL string + Module string + ModuleHash string + BaseURL string Angular AngularMeta @@ -532,3 +534,24 @@ func (pt Type) IsValid() bool { } return false } + +// PluginManifest holds details for the file manifest +type PluginManifest struct { + Plugin string `json:"plugin"` + Version string `json:"version"` + KeyID string `json:"keyId"` + Time int64 `json:"time"` + Files map[string]string `json:"files"` + + // V2 supported fields + ManifestVersion string `json:"manifestVersion"` + SignatureType SignatureType `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` + RootURLs []string `json:"rootUrls"` +} + +// IsV2 returns true if the manifest is version 2.x +func (m *PluginManifest) IsV2() bool { + return strings.HasPrefix(m.ManifestVersion, "2.") +} diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 218e9fabc36..5b4f3bed5bb 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -376,7 +376,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -714,8 +715,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) @@ -1042,7 +1042,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -1382,8 +1383,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index a5a7a7fd8db..eee04acedfc 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" + "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/provisionedplugins" @@ -213,10 +214,27 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1622547655175, + Files: map[string]string{ + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -647,10 +665,24 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { Executable: "test", State: plugins.ReleaseStateAlpha, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661171417046, + Files: map[string]string{ + "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "willbrowne", + SignedByOrgName: "Will Browne", + RootURLs: []string{"http://localhost:3000/"}, + }, Signature: "valid", SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -767,8 +799,22 @@ func TestLoader_Load_RBACReady(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1667484928676, + Files: map[string]string{ + "plugin.json": "3348335ec100392b325f3eeb882a07c729e9cbf0f1ae331239f46840bb1a01eb", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "gabrielmabille", + SignedByOrgName: "gabrielmabille", + RootURLs: []string{"http://localhost:3000/"}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "gabrielmabille", @@ -837,8 +883,22 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Backend: true, Executable: "test", }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661171981629, + Files: map[string]string{ + "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "willbrowne", + SignedByOrgName: "Will Browne", + RootURLs: []string{"http://localhost:3000/grafana"}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -925,8 +985,24 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1621356785895, + Files: map[string]string{ + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1017,8 +1093,24 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, pluginDir1), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, pluginDir1), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1621356785895, + Files: map[string]string{ + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1180,9 +1272,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: true, }, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661172777367, + Files: map[string]string{ + "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", + "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1225,9 +1331,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/test-panel/module.js", - BaseURL: "public/plugins/test-panel", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), + Module: "public/plugins/test-panel/module.js", + BaseURL: "public/plugins/test-panel", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661172777367, + Files: map[string]string{ + "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", + "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1375,10 +1495,25 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: false, }, - Module: "public/plugins/myorgid-simple-app/module.js", - BaseURL: "public/plugins/myorgid-simple-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), - DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + Module: "public/plugins/myorgid-simple-app/module.js", + BaseURL: "public/plugins/myorgid-simple-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), + DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + Manifest: &plugins.PluginManifest{ + Plugin: "myorgid-simple-app", + Version: "%VERSION%", + KeyID: "7e4d0c6a708866e7", + Time: 1642614241713, + Files: map[string]string{ + "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", + "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + RootURLs: []string{}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1431,6 +1566,21 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { BaseURL: "public/plugins/myorgid-simple-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, + Manifest: &plugins.PluginManifest{ + Plugin: "myorgid-simple-app", + Version: "%VERSION%", + KeyID: "7e4d0c6a708866e7", + Time: 1642614241713, + Files: map[string]string{ + "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", + "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + RootURLs: []string{}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1484,7 +1634,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi require.NoError(t, err) return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider, pluginscdn.ProvideService(cfg)), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) @@ -1514,7 +1664,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade } return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider()), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index f377e9eb867..94b5331ac00 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" @@ -42,7 +43,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service) }) } -func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider) *bootstrap.Bootstrap { +func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider, cdn *pluginscdn.Service) *bootstrap.Bootstrap { disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled { p.Alerting = false @@ -52,7 +53,7 @@ func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.Signature return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, ap), - DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc), + DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg, cdn), disableAlertingForTempoDecorateFunc), }) } diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets.go b/pkg/services/pluginsintegration/pluginassets/pluginassets.go index 4d9a7ec1a53..8735f7a4354 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets.go @@ -2,19 +2,12 @@ package pluginassets import ( "context" - "encoding/base64" - "encoding/hex" - "fmt" - "path" - "path/filepath" - "sync" "github.com/Masterminds/semver/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -28,24 +21,20 @@ var ( scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled) ) -func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service { +func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service { return &Service{ - cfg: cfg, - cdn: cdn, - signature: sig, - store: store, - log: log.New("pluginassets"), + cfg: cfg, + cdn: cdn, + store: store, + log: log.New("pluginassets"), } } type Service struct { - cfg *config.PluginManagementCfg - cdn *pluginscdn.Service - signature *signature.Signature - store pluginstore.Store - log log.Logger - - moduleHashCache sync.Map + cfg *config.PluginManagementCfg + cdn *pluginscdn.Service + store pluginstore.Store + log log.Logger } // LoadingStrategy calculates the loading strategy for a plugin. @@ -82,95 +71,6 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi return plugins.LoadingStrategyFetch } -// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. -// The module hash is read from the plugin's MANIFEST.txt file. -// The plugin can also be a nested plugin. -// If the plugin is unsigned, an empty string is returned. -// The results are cached to avoid repeated reads from the MANIFEST.txt file. -func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string { - k := s.moduleHashCacheKey(p) - cachedValue, ok := s.moduleHashCache.Load(k) - if ok { - return cachedValue.(string) - } - mh, err := s.moduleHash(ctx, p, "") - if err != nil { - s.log.Error("Failed to calculate module hash", "plugin", p.ID, "error", err) - } - s.moduleHashCache.Store(k, mh) - return mh -} - -// moduleHash is the underlying function for ModuleHash. See its documentation for more information. -// If the plugin is not a CDN plugin, the function will return an empty string. -// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin. -// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's -// module.js file, rather than for the provided plugin. -func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSBase string) (r string, err error) { - if !s.cfg.Features.SriChecksEnabled { - return "", nil - } - - // Ignore unsigned plugins - if !p.Signature.IsValid() { - return "", nil - } - - if p.Parent != nil { - // Nested plugin - parent, ok := s.store.Plugin(ctx, p.Parent.ID) - if !ok { - return "", fmt.Errorf("parent plugin plugin %q for child plugin %q not found", p.Parent.ID, p.ID) - } - - // The module hash is contained within the parent's MANIFEST.txt file. - // For example, the parent's MANIFEST.txt will contain an entry similar to this: - // - // ``` - // "datasource/module.js": "1234567890abcdef..." - // ``` - // - // Recursively call moduleHash with the parent plugin and with the children plugin folder path - // to get the correct module hash for the nested plugin. - if childFSBase == "" { - childFSBase = p.Base() - } - return s.moduleHash(ctx, parent, childFSBase) - } - - // Only CDN plugins are supported for SRI checks. - // CDN plugins have the version as part of the URL, which acts as a cache-buster. - // Needed due to: https://github.com/grafana/plugin-tools/pull/1426 - // FS plugins build before this change will have SRI mismatch issues. - if !s.cdnEnabled(p.ID, p.FS) { - return "", nil - } - - manifest, err := s.signature.ReadPluginManifestFromFS(ctx, p.FS) - if err != nil { - return "", fmt.Errorf("read plugin manifest: %w", err) - } - if !manifest.IsV2() { - return "", nil - } - - var childPath string - if childFSBase != "" { - // Calculate the relative path of the child plugin folder from the parent plugin folder. - childPath, err = p.FS.Rel(childFSBase) - if err != nil { - return "", fmt.Errorf("rel path: %w", err) - } - // MANIFETS.txt uses forward slashes as path separators. - childPath = filepath.ToSlash(childPath) - } - moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")] - if !ok { - return "", nil - } - return convertHashForSRI(moduleHash) -} - func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { if cpv, ok := ps[CreatePluginVersionCfgKey]; ok { createPluginVer, err := semver.NewVersion(cpv) @@ -188,17 +88,3 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool { return s.cdn.PluginSupported(pluginID) || fs.Type().CDN() } - -// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. -func convertHashForSRI(h string) (string, error) { - hb, err := hex.DecodeString(h) - if err != nil { - return "", fmt.Errorf("hex decode string: %w", err) - } - return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil -} - -// moduleHashCacheKey returns a unique key for the module hash cache. -func (s *Service) moduleHashCacheKey(p pluginstore.Plugin) string { - return p.ID + ":" + p.Info.Version -} diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go index 192717c34ff..91b8b515bb1 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go @@ -2,19 +2,14 @@ package pluginassets import ( "context" - "fmt" - "path/filepath" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -179,349 +174,6 @@ func TestService_Calculate(t *testing.T) { } } -func TestService_ModuleHash(t *testing.T) { - const ( - pluginID = "grafana-test-datasource" - parentPluginID = "grafana-test-app" - ) - for _, tc := range []struct { - name string - features *config.Features - store []pluginstore.Plugin - - // Can be used to configure plugin's fs - // fs cdn type = loaded from CDN with no files on disk - // fs local type = files on disk but served from CDN only if cdn=true - plugin pluginstore.Plugin - - // When true, set cdn=true in config - cdn bool - expModuleHash string - }{ - { - name: "unsigned should not return module hash", - plugin: newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned)), - cdn: false, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassExternal), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassExternal), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - // parentPluginID (/) - // └── pluginID (/datasource) - name: "nested plugin should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - parentPluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ), - }, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))), - withParent(parentPluginID), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"), - }, - { - // parentPluginID (/) - // └── pluginID (/panels/one) - name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - parentPluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ), - }, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), - withParent(parentPluginID), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), - }, - { - // grand-parent-app (/) - // ├── parent-datasource (/datasource) - // │ └── child-panel (/datasource/panels/one) - name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - "grand-parent-app", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))), - ), - newPlugin( - "parent-datasource", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))), - withParent("grand-parent-app"), - ), - }, - plugin: newPlugin( - "child-panel", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))), - withParent("parent-datasource"), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), - }, - { - name: "nested plugin should not return module hash from parent if it's not registered in the store", - store: []pluginstore.Plugin{}, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), - withParent(parentPluginID), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - name: "missing module.js entry from MANIFEST.txt should not return module hash", - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - name: "signed status but missing MANIFEST.txt should not return module hash", - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - } { - if tc.name == "" { - var expS string - if tc.expModuleHash == "" { - expS = "should not return module hash" - } else { - expS = "should return module hash" - } - tc.name = fmt.Sprintf("feature=%v, cdn_config=%v, class=%v %s", tc.features.SriChecksEnabled, tc.cdn, tc.plugin.Class, expS) - } - - t.Run(tc.name, func(t *testing.T) { - var pluginSettings config.PluginSettings - if tc.cdn { - pluginSettings = config.PluginSettings{ - pluginID: { - "cdn": "true", - }, - parentPluginID: map[string]string{ - "cdn": "true", - }, - "grand-parent-app": map[string]string{ - "cdn": "true", - }, - } - } - features := tc.features - if features == nil { - features = &config.Features{} - } - pCfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "http://cdn.example.com", - PluginSettings: pluginSettings, - Features: *features, - } - svc := ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(tc.store...), - ) - mh := svc.ModuleHash(context.Background(), tc.plugin) - require.Equal(t, tc.expModuleHash, mh) - }) - } -} - -func TestService_ModuleHash_Cache(t *testing.T) { - pCfg := &config.PluginManagementCfg{ - PluginSettings: config.PluginSettings{}, - Features: config.Features{SriChecksEnabled: true}, - } - svc := ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(), - ) - const pluginID = "grafana-test-datasource" - - t.Run("cache key", func(t *testing.T) { - t.Run("with version", func(t *testing.T) { - const pluginVersion = "1.0.0" - p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion})) - k := svc.moduleHashCacheKey(p) - require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct") - }) - - t.Run("without version", func(t *testing.T) { - p := newPlugin(pluginID) - k := svc.moduleHashCacheKey(p) - require.Equal(t, pluginID+":", k, "cache key should be correct") - }) - }) - - t.Run("ModuleHash usage", func(t *testing.T) { - pV1 := newPlugin( - pluginID, - withInfo(plugins.Info{Version: "1.0.0"}), - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ) - - pCfg = &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.grafana.com", - PluginSettings: config.PluginSettings{ - pluginID: { - "cdn": "true", - }, - }, - Features: config.Features{SriChecksEnabled: true}, - } - svc = ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(), - ) - - k := svc.moduleHashCacheKey(pV1) - - _, ok := svc.moduleHashCache.Load(k) - require.False(t, ok, "cache should initially be empty") - - mhV1 := svc.ModuleHash(context.Background(), pV1) - pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03") - require.Equal(t, pV1Exp, mhV1, "returned value should be correct") - - cachedMh, ok := svc.moduleHashCache.Load(k) - require.True(t, ok) - require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value") - - t.Run("different version uses different cache key", func(t *testing.T) { - pV2 := newPlugin( - pluginID, - withInfo(plugins.Info{Version: "2.0.0"}), - withSignatureStatus(plugins.SignatureStatusValid), - // different fs for different hash - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ) - mhV2 := svc.ModuleHash(context.Background(), pV2) - require.NotEqual(t, mhV2, mhV1, "different version should have different hash") - require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2) - }) - - t.Run("cache should be used", func(t *testing.T) { - // edit cache directly - svc.moduleHashCache.Store(k, "hax") - require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1)) - }) - }) -} - -func TestConvertHashFromSRI(t *testing.T) { - for _, tc := range []struct { - hash string - expHash string - expErr bool - }{ - { - hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", - expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", - }, - { - hash: "not-a-valid-hash", - expErr: true, - }, - } { - t.Run(tc.hash, func(t *testing.T) { - r, err := convertHashForSRI(tc.hash) - if tc.expErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tc.expHash, r) - } - }) - } -} - func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin { p := pluginstore.Plugin{ JSONData: plugins.JSONData{ @@ -534,13 +186,6 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl return p } -func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Info = info - return p - } -} - func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.FS = fs @@ -548,13 +193,6 @@ func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { } } -func withSignatureStatus(status plugins.SignatureStatus) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Signature = status - return p - } -} - func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Angular = plugins.AngularMeta{Detected: angular} @@ -562,13 +200,6 @@ func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { } } -func withParent(parentID string) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Parent = &pluginstore.ParentPlugin{ID: parentID} - return p - } -} - func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Class = class @@ -587,9 +218,3 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti pluginID: kv, } } - -func newSRIHash(t *testing.T, s string) string { - r, err := convertHashForSRI(s) - require.NoError(t, err) - return r -} diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index f4504d254c5..77fe1365fc3 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -30,8 +30,9 @@ type Plugin struct { Error *plugins.Error // SystemJS fields - Module string - BaseURL string + Module string + BaseURL string + ModuleHash string Angular plugins.AngularMeta @@ -80,6 +81,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { ExternalService: p.ExternalService, Angular: p.Angular, Translations: p.Translations, + ModuleHash: p.ModuleHash, } if p.Parent != nil { diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 9daad43e3e2..9957fc11b22 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" @@ -49,7 +50,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core proc := process.ProvideService() disc := pipeline.ProvideDiscoveryStage(pCfg, reg) - boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider()) + boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(pCfg)) valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector) init := pipeline.ProvideInitializationStage(pCfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()) term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) @@ -87,7 +88,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo } if opts.Bootstrapper == nil { - opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider()) + opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)) } if opts.Validator == nil {