The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/plugins/pluginassets/modulehash/modulehash_test.go

459 lines
14 KiB

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
}