Compare commits
7 Commits
ash/react-
...
wb/pkg-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c6e870a3a | ||
|
|
975480f10e | ||
|
|
3445cd3378 | ||
|
|
df790a4279 | ||
|
|
f3e28cf440 | ||
|
|
f8ab6ecc79 | ||
|
|
6a955eb6d1 |
@@ -20,8 +20,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
@@ -80,7 +82,8 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
var pluginsAssets = passets
|
var pluginsAssets = passets
|
||||||
if pluginsAssets == nil {
|
if pluginsAssets == nil {
|
||||||
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
||||||
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore)
|
calc := modulehash.NewCalculator(pluginsCfg, registry.NewInMemory(), pluginsCDN, sig)
|
||||||
|
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, calc)
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
@@ -714,6 +717,8 @@ func newPluginAssets() func() *pluginassets.Service {
|
|||||||
|
|
||||||
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
|
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
|
||||||
return func() *pluginassets.Service {
|
return func() *pluginassets.Service {
|
||||||
return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{})
|
cdn := pluginscdn.ProvideService(pCfg)
|
||||||
|
calc := modulehash.NewCalculator(pCfg, registry.NewInMemory(), cdn, signature.ProvideService(pCfg, statickey.New()))
|
||||||
|
return pluginassets.ProvideService(pCfg, cdn, calc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
@@ -849,7 +850,8 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
pCfg := &config.PluginManagementCfg{}
|
pCfg := &config.PluginManagementCfg{}
|
||||||
pluginCDN := pluginscdn.ProvideService(pCfg)
|
pluginCDN := pluginscdn.ProvideService(pCfg)
|
||||||
sig := signature.ProvideService(pCfg, statickey.New())
|
sig := signature.ProvideService(pCfg, statickey.New())
|
||||||
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore)
|
calc := modulehash.NewCalculator(pCfg, registry.NewInMemory(), pluginCDN, sig)
|
||||||
|
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, calc)
|
||||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||||
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
|
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
|
||||||
hs.Cfg,
|
hs.Cfg,
|
||||||
|
|||||||
144
pkg/plugins/pluginassets/modulehash/modulehash.go
Normal file
144
pkg/plugins/pluginassets/modulehash/modulehash.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package modulehash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Calculator struct {
|
||||||
|
reg registry.Service
|
||||||
|
cfg *config.PluginManagementCfg
|
||||||
|
cdn *pluginscdn.Service
|
||||||
|
signature *signature.Signature
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
moduleHashCache sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCalculator(cfg *config.PluginManagementCfg, reg registry.Service, cdn *pluginscdn.Service, signature *signature.Signature) *Calculator {
|
||||||
|
return &Calculator{
|
||||||
|
cfg: cfg,
|
||||||
|
reg: reg,
|
||||||
|
cdn: cdn,
|
||||||
|
signature: signature,
|
||||||
|
log: log.New("modulehash"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (c *Calculator) ModuleHash(ctx context.Context, pluginID, pluginVersion string) string {
|
||||||
|
p, ok := c.reg.Plugin(ctx, pluginID, pluginVersion)
|
||||||
|
if !ok {
|
||||||
|
c.log.Error("Failed to calculate module hash as plugin is not registered", "pluginId", pluginID)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
k := c.moduleHashCacheKey(pluginID, pluginVersion)
|
||||||
|
cachedValue, ok := c.moduleHashCache.Load(k)
|
||||||
|
if ok {
|
||||||
|
return cachedValue.(string)
|
||||||
|
}
|
||||||
|
mh, err := c.moduleHash(ctx, p, "")
|
||||||
|
if err != nil {
|
||||||
|
c.log.Error("Failed to calculate module hash", "pluginId", p.ID, "error", err)
|
||||||
|
}
|
||||||
|
c.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 (c *Calculator) moduleHash(ctx context.Context, p *plugins.Plugin, childFSBase string) (r string, err error) {
|
||||||
|
if !c.cfg.Features.SriChecksEnabled {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore unsigned plugins
|
||||||
|
if !p.Signature.IsValid() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Parent != nil {
|
||||||
|
// 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.FS.Base()
|
||||||
|
}
|
||||||
|
return c.moduleHash(ctx, p.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 !c.cdnEnabled(p.ID, p.FS) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := c.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 (c *Calculator) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
||||||
|
return c.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 (c *Calculator) moduleHashCacheKey(pluginId, pluginVersion string) string {
|
||||||
|
return pluginId + ":" + pluginVersion
|
||||||
|
}
|
||||||
459
pkg/plugins/pluginassets/modulehash/modulehash_test.go
Normal file
459
pkg/plugins/pluginassets/modulehash/modulehash_test.go
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
package modulehash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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/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/pluginscdn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ModuleHash(t *testing.T) {
|
||||||
|
const (
|
||||||
|
pluginID = "grafana-test-datasource"
|
||||||
|
parentPluginID = "grafana-test-app"
|
||||||
|
)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
features *config.Features
|
||||||
|
registry []*plugins.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 string
|
||||||
|
|
||||||
|
// When true, set cdn=true in config
|
||||||
|
cdn bool
|
||||||
|
expModuleHash string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unsigned should not return module hash",
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.Plugin{newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned))},
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: false},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.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: pluginID,
|
||||||
|
registry: []*plugins.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: pluginID,
|
||||||
|
registry: []*plugins.Plugin{newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
||||||
|
)},
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.Plugin{newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid"))),
|
||||||
|
)},
|
||||||
|
cdn: true,
|
||||||
|
features: &config.Features{SriChecksEnabled: false},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.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",
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "datasource"))),
|
||||||
|
withParent(newPlugin(
|
||||||
|
parentPluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||||
|
withParent(newPlugin(
|
||||||
|
parentPluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
registry: []*plugins.Plugin{
|
||||||
|
newPlugin(
|
||||||
|
"child-panel",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))),
|
||||||
|
withParent(newPlugin(
|
||||||
|
"parent-datasource",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested", "datasource"))),
|
||||||
|
withParent(newPlugin(
|
||||||
|
"grand-parent-app",
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-deeply-nested"))),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
plugin: "child-panel",
|
||||||
|
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 registry",
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.Plugin{newPlugin(
|
||||||
|
pluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||||
|
withParent(newPlugin(
|
||||||
|
parentPluginID,
|
||||||
|
withSignatureStatus(plugins.SignatureStatusValid),
|
||||||
|
withFS(plugins.NewLocalFS(filepath.Join("../testdata", "module-hash-valid-nested"))),
|
||||||
|
)),
|
||||||
|
)},
|
||||||
|
cdn: false,
|
||||||
|
features: &config.Features{SriChecksEnabled: true},
|
||||||
|
expModuleHash: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing module.js entry from MANIFEST.txt should not return module hash",
|
||||||
|
plugin: pluginID,
|
||||||
|
registry: []*plugins.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: pluginID,
|
||||||
|
registry: []*plugins.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 %s", tc.features.SriChecksEnabled, tc.cdn, 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 := NewCalculator(
|
||||||
|
pCfg,
|
||||||
|
newPluginRegistry(t, tc.registry...),
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
)
|
||||||
|
mh := svc.ModuleHash(context.Background(), tc.plugin, "")
|
||||||
|
require.Equal(t, tc.expModuleHash, mh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ModuleHash_Cache(t *testing.T) {
|
||||||
|
pCfg := &config.PluginManagementCfg{
|
||||||
|
PluginSettings: config.PluginSettings{},
|
||||||
|
Features: config.Features{SriChecksEnabled: true},
|
||||||
|
}
|
||||||
|
svc := NewCalculator(
|
||||||
|
pCfg,
|
||||||
|
newPluginRegistry(t),
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
)
|
||||||
|
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.ID, p.Info.Version)
|
||||||
|
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.ID, p.Info.Version)
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
reg := newPluginRegistry(t, pV1)
|
||||||
|
svc = NewCalculator(
|
||||||
|
pCfg,
|
||||||
|
reg,
|
||||||
|
pluginscdn.ProvideService(pCfg),
|
||||||
|
signature.ProvideService(pCfg, statickey.New()),
|
||||||
|
)
|
||||||
|
|
||||||
|
k := svc.moduleHashCacheKey(pV1.ID, pV1.Info.Version)
|
||||||
|
|
||||||
|
_, ok := svc.moduleHashCache.Load(k)
|
||||||
|
require.False(t, ok, "cache should initially be empty")
|
||||||
|
|
||||||
|
mhV1 := svc.ModuleHash(context.Background(), pV1.ID, pV1.Info.Version)
|
||||||
|
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"))),
|
||||||
|
)
|
||||||
|
err := reg.Add(context.Background(), pV2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mhV2 := svc.ModuleHash(context.Background(), pV2.ID, pV2.Info.Version)
|
||||||
|
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.ID, pV1.Info.Version))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *plugins.Plugin) *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p := &plugins.Plugin{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: pluginID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, cb := range cbs {
|
||||||
|
p = cb(p)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func withInfo(info plugins.Info) func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
return func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p.Info = info
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withFS(fs plugins.FS) func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
return func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p.FS = fs
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withSignatureStatus(status plugins.SignatureStatus) func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
return func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p.Signature = status
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withParent(parent *plugins.Plugin) func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
return func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p.Parent = parent
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withClass(class plugins.Class) func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
return func(p *plugins.Plugin) *plugins.Plugin {
|
||||||
|
p.Class = class
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSRIHash(t *testing.T, s string) string {
|
||||||
|
r, err := convertHashForSRI(s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginRegistry struct {
|
||||||
|
registry.Service
|
||||||
|
|
||||||
|
reg map[string]*plugins.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPluginRegistry(t *testing.T, ps ...*plugins.Plugin) *pluginRegistry {
|
||||||
|
reg := &pluginRegistry{
|
||||||
|
reg: make(map[string]*plugins.Plugin),
|
||||||
|
}
|
||||||
|
for _, p := range ps {
|
||||||
|
err := reg.Add(context.Background(), p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *pluginRegistry) Plugin(_ context.Context, id, version string) (*plugins.Plugin, bool) {
|
||||||
|
key := fmt.Sprintf("%s-%s", id, version)
|
||||||
|
p, exists := f.reg[key]
|
||||||
|
return p, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *pluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
|
||||||
|
key := fmt.Sprintf("%s-%s", p.ID, p.Info.Version)
|
||||||
|
f.reg[key] = p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
6
pkg/server/wire_gen.go
generated
6
pkg/server/wire_gen.go
generated
@@ -715,7 +715,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
calculator := pluginassets2.ProvideModuleHashCalculator(pluginManagementCfg, pluginscdnService, signatureSignature, inMemory)
|
||||||
|
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, calculator)
|
||||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||||
@@ -1383,7 +1384,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
calculator := pluginassets2.ProvideModuleHashCalculator(pluginManagementCfg, pluginscdnService, signatureSignature, inMemory)
|
||||||
|
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, calculator)
|
||||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||||
|
|||||||
@@ -2,19 +2,15 @@ package pluginassets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginassets/modulehash"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
)
|
)
|
||||||
@@ -28,24 +24,27 @@ var (
|
|||||||
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
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,
|
||||||
|
calc *modulehash.Calculator) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cdn: cdn,
|
cdn: cdn,
|
||||||
signature: sig,
|
log: log.New("pluginassets"),
|
||||||
store: store,
|
calc: calc,
|
||||||
log: log.New("pluginassets"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
func ProvideModuleHashCalculator(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service,
|
||||||
cfg *config.PluginManagementCfg
|
signature *signature.Signature, reg registry.Service) *modulehash.Calculator {
|
||||||
cdn *pluginscdn.Service
|
return modulehash.NewCalculator(cfg, reg, cdn, signature)
|
||||||
signature *signature.Signature
|
}
|
||||||
store pluginstore.Store
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
moduleHashCache sync.Map
|
type Service struct {
|
||||||
|
cfg *config.PluginManagementCfg
|
||||||
|
cdn *pluginscdn.Service
|
||||||
|
calc *modulehash.Calculator
|
||||||
|
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadingStrategy calculates the loading strategy for a plugin.
|
// LoadingStrategy calculates the loading strategy for a plugin.
|
||||||
@@ -83,92 +82,8 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
// 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 {
|
func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string {
|
||||||
k := s.moduleHashCacheKey(p)
|
return s.calc.ModuleHash(ctx, p.ID, p.Info.Version)
|
||||||
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 {
|
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||||
@@ -188,17 +103,3 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
|||||||
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
||||||
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
"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/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"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 {
|
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p := pluginstore.Plugin{
|
p := pluginstore.Plugin{
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
@@ -534,13 +186,6 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl
|
|||||||
return p
|
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 {
|
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.FS = fs
|
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 {
|
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Angular = plugins.AngularMeta{Detected: angular}
|
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 {
|
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Class = class
|
p.Class = class
|
||||||
@@ -587,9 +218,3 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti
|
|||||||
pluginID: kv,
|
pluginID: kv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSRIHash(t *testing.T, s string) string {
|
|
||||||
r, err := convertHashForSRI(s)
|
|
||||||
require.NoError(t, err)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ var WireSet = wire.NewSet(
|
|||||||
plugincontext.ProvideBaseService,
|
plugincontext.ProvideBaseService,
|
||||||
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
||||||
plugininstaller.ProvideService,
|
plugininstaller.ProvideService,
|
||||||
|
pluginassets.ProvideModuleHashCalculator,
|
||||||
pluginassets.ProvideService,
|
pluginassets.ProvideService,
|
||||||
pluginchecker.ProvidePreinstall,
|
pluginchecker.ProvidePreinstall,
|
||||||
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
|
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
|
||||||
|
|||||||
Reference in New Issue
Block a user