mirror of
https://github.com/grafana/grafana.git
synced 2026-01-13 07:24:10 +08:00
Compare commits
4 Commits
main
...
wb/module-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7146f39d2 | ||
|
|
9443b42578 | ||
|
|
fcb76c0117 | ||
|
|
be27b3295e |
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
90
pkg/plugins/pluginassets/modulehash.go
Normal file
90
pkg/plugins/pluginassets/modulehash.go
Normal file
@@ -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)
|
||||
}
|
||||
356
pkg/plugins/pluginassets/modulehash_test.go
Normal file
356
pkg/plugins/pluginassets/modulehash_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
12
pkg/server/wire_gen.go
generated
12
pkg/server/wire_gen.go
generated
@@ -375,7 +375,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)
|
||||
@@ -713,8 +714,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)
|
||||
@@ -1035,7 +1035,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)
|
||||
@@ -1375,8 +1376,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user