mirror of
https://github.com/grafana/grafana.git
synced 2025-01-12 09:02:30 -06:00
Plugins: Add Subresource Integrity checks (#93024)
* Plugins: Pass hashes for SRI to frontend * Add SRI hashes to frontendsettings DTOs * Add docstring * TestSriHashes * Fix typo * Changed SriHashes to ModuleHash * update loader_test compareOpts * update ModuleHash error message * Add TestModuleHash/no_module.js * Add omitEmpty to moduleHash * Add ModuleHash to api/plugins/${pluginId}/settings * moved ModuleHash field * feat(plugins): add moduleHash to bootData and plugin types * feat(plugins): if moduleHash is available apply it to systemjs importmap * Calculate ModuleHash for CDN provisioned plugins * Add ModuleHash tests for TestCalculate * adjust test case name * removed .envrc * Fix signature verification failing for internal plugins * fix tests * Add pluginsFilesystemSriChecks feature togglemk * renamed FilesystemSriChecksEnabled * refactor(plugin_loader): prefer extending type declaration over ts-error * added a couple more tests * Removed unused features * Removed unused argument from signature.DefaultCalculator call * Removed unused argument from bootstrap.DefaultConstructFunc * Moved ModuleHash to pluginassets service * update docstring * lint * Removed cdn dependency from manifest.Signature * add tests * fix extra parameters in tests * "fix" tests * removed outdated test * removed unused cdn dependency in signature.DefaultCalculator * reduce diff * Cache returned values * Add support for deeply nested plugins (more than 1 hierarchy level) * simplify cache usage * refactor TestService_ModuleHash_Cache * removed unused testdata * re-generate feature toggles * use version for module hash cache * Renamed feature toggle to pluginsSriChecks and use it for both cdn and filesystem * Removed app/types/system-integrity.d.ts * re-generate feature toggles * re-generate feature toggles * feat(plugins): put systemjs integrity hash behind feature flag --------- Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
parent
153036be2e
commit
0db65d229e
@ -207,6 +207,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time |
|
||||
| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions |
|
||||
| `rolePickerDrawer` | Enables the new role picker drawer design |
|
||||
| `pluginsSriChecks` | Enables SRI checks for plugin assets |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
@ -219,4 +219,5 @@ export interface FeatureToggles {
|
||||
useSessionStorageForRedirection?: boolean;
|
||||
rolePickerDrawer?: boolean;
|
||||
unifiedStorageSearch?: boolean;
|
||||
pluginsSriChecks?: boolean;
|
||||
}
|
||||
|
@ -99,6 +99,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
||||
angularDetected?: boolean;
|
||||
loadingStrategy?: PluginLoadingStrategy;
|
||||
extensions?: PluginExtensions;
|
||||
moduleHash?: string;
|
||||
}
|
||||
|
||||
interface PluginDependencyInfo {
|
||||
|
@ -46,6 +46,7 @@ export type AppPluginConfig = {
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
dependencies: PluginDependencies;
|
||||
extensions: PluginExtensions;
|
||||
moduleHash?: string;
|
||||
};
|
||||
|
||||
export type PreinstalledPlugin = {
|
||||
|
@ -30,6 +30,7 @@ type PluginSetting struct {
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
LoadingStrategy plugins.LoadingStrategy `json:"loadingStrategy"`
|
||||
ModuleHash string `json:"moduleHash,omitempty"`
|
||||
}
|
||||
|
||||
type PluginListItem struct {
|
||||
|
@ -145,6 +145,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),
|
||||
BaseURL: panel.BaseURL,
|
||||
SkipDataQuery: panel.SkipDataQuery,
|
||||
HideFromList: panel.HideFromList,
|
||||
@ -453,6 +454,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),
|
||||
BaseURL: plugin.BaseURL,
|
||||
Angular: plugin.Angular,
|
||||
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||
@ -538,8 +540,9 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
||||
JSONData: ds.JSONData,
|
||||
Signature: ds.Signature,
|
||||
Module: ds.Module,
|
||||
BaseURL: ds.BaseURL,
|
||||
Angular: ds.Angular,
|
||||
// ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), ds),
|
||||
BaseURL: ds.BaseURL,
|
||||
Angular: ds.Angular,
|
||||
},
|
||||
}
|
||||
if ds.Name == grafanads.DatasourceName {
|
||||
@ -563,6 +566,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),
|
||||
}
|
||||
|
||||
if settings.Enabled {
|
||||
|
@ -18,6 +18,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social/socialimpl"
|
||||
"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/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"
|
||||
@ -51,10 +53,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
||||
})
|
||||
}
|
||||
|
||||
pluginsCDN := pluginscdn.ProvideService(&config.PluginManagementCfg{
|
||||
pluginsCfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
||||
PluginSettings: cfg.PluginSettings,
|
||||
})
|
||||
}
|
||||
pluginsCDN := pluginscdn.ProvideService(pluginsCfg)
|
||||
|
||||
var pluginStore = pstore
|
||||
if pluginStore == nil {
|
||||
@ -68,7 +71,8 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
||||
|
||||
var pluginsAssets = passets
|
||||
if pluginsAssets == nil {
|
||||
pluginsAssets = pluginassets.ProvideService(cfg, pluginsCDN)
|
||||
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
||||
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore)
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
@ -240,6 +244,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{
|
||||
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||
// ModuleHash: "sha256-test",
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Info: plugins.Info{Version: "0.5.0"},
|
||||
@ -255,9 +260,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", false),
|
||||
}
|
||||
},
|
||||
pluginAssets: func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@ -266,6 +269,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Path: "/test-app/module.js",
|
||||
Version: "0.5.0",
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
// ModuleHash: "sha256-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -277,6 +281,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{
|
||||
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||
// ModuleHash: "sha256-test",
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Info: plugins.Info{Version: "0.5.0"},
|
||||
@ -292,9 +297,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@ -303,6 +306,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Path: "/test-app/module.js",
|
||||
Version: "0.5.0",
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
// ModuleHash: "sha256-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -330,9 +334,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@ -368,15 +370,13 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(&setting.Cfg{
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"test-app": {
|
||||
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
|
||||
},
|
||||
pluginAssets: newPluginAssetsWithConfig(&config.PluginManagementCfg{
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"test-app": {
|
||||
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
|
||||
},
|
||||
}, pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@ -412,9 +412,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@ -456,3 +454,13 @@ func newAppSettings(id string, enabled bool) map[string]*pluginsettings.DTO {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newPluginAssets() func() *pluginassets.Service {
|
||||
return newPluginAssetsWithConfig(&config.PluginManagementCfg{})
|
||||
}
|
||||
|
||||
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{})
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +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),
|
||||
DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
|
||||
State: plugin.State,
|
||||
Signature: plugin.Signature,
|
||||
|
@ -27,6 +27,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
|
||||
"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/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@ -788,7 +790,6 @@ func Test_PluginsSettings(t *testing.T) {
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
}}, plugins.ClassExternal, plugins.NewFakeFS())
|
||||
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p1.ID: p1,
|
||||
@ -843,8 +844,10 @@ func Test_PluginsSettings(t *testing.T) {
|
||||
ErrorCode: tc.errCode,
|
||||
})
|
||||
}
|
||||
pluginCDN := pluginscdn.ProvideService(&config.PluginManagementCfg{})
|
||||
hs.pluginAssets = pluginassets.ProvideService(hs.Cfg, pluginCDN)
|
||||
pCfg := &config.PluginManagementCfg{}
|
||||
pluginCDN := pluginscdn.ProvideService(pCfg)
|
||||
sig := signature.ProvideService(pCfg, statickey.New())
|
||||
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore)
|
||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||
var err error
|
||||
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
@ -32,6 +32,7 @@ type PluginManagementCfg struct {
|
||||
type Features struct {
|
||||
ExternalCorePluginsEnabled bool
|
||||
SkipHostEnvVarsEnabled bool
|
||||
SriChecksEnabled bool
|
||||
}
|
||||
|
||||
// NewPluginManagementCfg returns a new PluginManagementCfg.
|
||||
|
@ -53,7 +53,7 @@ type PluginManifest struct {
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *PluginManifest) isV2() bool {
|
||||
func (m *PluginManifest) IsV2() bool {
|
||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||
}
|
||||
|
||||
@ -107,34 +107,17 @@ func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*Plugi
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
fsFiles, err := plugin.FS.Files()
|
||||
if err != nil {
|
||||
return plugins.Signature{}, 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
|
||||
}
|
||||
var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned")
|
||||
|
||||
f, err := plugin.FS.Open("MANIFEST.txt")
|
||||
// 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) {
|
||||
f, err := pfs.Open("MANIFEST.txt")
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
s.log.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("%w: could not find a MANIFEST.txt", ErrSignatureTypeUnsigned)
|
||||
}
|
||||
|
||||
s.log.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("could not open MANIFEST.txt: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if f == nil {
|
||||
@ -147,21 +130,47 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
|
||||
byteValue, err := io.ReadAll(f)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
s.log.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
return nil, fmt.Errorf("%w: MANIFEST.txt is invalid", ErrSignatureTypeUnsigned)
|
||||
}
|
||||
|
||||
manifest, err := s.readPluginManifest(ctx, byteValue)
|
||||
if err != nil {
|
||||
s.log.Warn("Plugin signature invalid", "id", plugin.JSONData.ID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
|
||||
manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS)
|
||||
switch {
|
||||
case errors.Is(err, ErrSignatureTypeUnsigned):
|
||||
s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
case err != nil:
|
||||
s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !manifest.isV2() {
|
||||
if !manifest.IsV2() {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
fsFiles, err := plugin.FS.Files()
|
||||
if err != nil {
|
||||
return plugins.Signature{}, 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
|
||||
@ -328,7 +337,7 @@ func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, bloc
|
||||
if len(m.Files) == 0 {
|
||||
return invalidFieldErr{field: "files"}
|
||||
}
|
||||
if m.isV2() {
|
||||
if m.IsV2() {
|
||||
if len(m.SignedByOrg) == 0 {
|
||||
return invalidFieldErr{field: "signedByOrg"}
|
||||
}
|
||||
|
@ -19,6 +19,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
)
|
||||
|
||||
func provideDefaultTestService() *Signature {
|
||||
return provideTestServiceWithConfig(&config.PluginManagementCfg{})
|
||||
}
|
||||
|
||||
func provideTestServiceWithConfig(cfg *config.PluginManagementCfg) *Signature {
|
||||
return ProvideService(cfg, statickey.New())
|
||||
}
|
||||
|
||||
func TestReadPluginManifest(t *testing.T) {
|
||||
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
@ -52,7 +60,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@ -68,8 +76,8 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
})
|
||||
|
||||
t.Run("invalid manifest", func(t *testing.T) {
|
||||
s := provideDefaultTestService()
|
||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
||||
require.Error(t, err)
|
||||
})
|
||||
@ -107,7 +115,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@ -126,6 +134,12 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
}
|
||||
|
||||
func TestCalculate(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Validate root URL against App URL for non-private plugin if is specified in manifest", func(t *testing.T) {
|
||||
tcs := []struct {
|
||||
appURL string
|
||||
@ -147,15 +161,9 @@ func TestCalculate(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
||||
s := ProvideService(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}, statickey.New())
|
||||
s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL})
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
@ -183,7 +191,7 @@ func TestCalculate(t *testing.T) {
|
||||
basePath := "../testdata/renderer-added-file/plugin"
|
||||
|
||||
runningWindows = true
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
@ -247,7 +255,7 @@ func TestCalculate(t *testing.T) {
|
||||
toSlash = tc.platform.toSlashFunc()
|
||||
fromSlash = tc.platform.fromSlashFunc()
|
||||
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
pfs, err := tc.fsFactory()
|
||||
require.NoError(t, err)
|
||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||
@ -721,7 +729,7 @@ func Test_validateManifest(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
||||
s := provideDefaultTestService()
|
||||
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
||||
require.Errorf(t, err, tc.expectedErr)
|
||||
})
|
||||
|
@ -262,6 +262,7 @@ type PluginMetaDTO struct {
|
||||
JSONData
|
||||
Signature SignatureStatus `json:"signature"`
|
||||
Module string `json:"module"`
|
||||
ModuleHash string `json:"moduleHash,omitempty"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Angular AngularMeta `json:"angular"`
|
||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||
@ -314,6 +315,7 @@ type PanelDTO struct {
|
||||
Module string `json:"module"`
|
||||
Angular AngularMeta `json:"angular"`
|
||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||
ModuleHash string `json:"moduleHash,omitempty"`
|
||||
}
|
||||
|
||||
type AppDTO struct {
|
||||
@ -325,6 +327,7 @@ type AppDTO struct {
|
||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||
Extensions Extensions `json:"extensions"`
|
||||
Dependencies Dependencies `json:"dependencies"`
|
||||
ModuleHash string `json:"moduleHash,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -1514,6 +1514,12 @@ var (
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "pluginsSriChecks",
|
||||
Description: "Enables SRI checks for plugin assets",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -200,3 +200,4 @@ improvedExternalSessionHandling,experimental,@grafana/identity-access-team,false
|
||||
useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,false,false
|
||||
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
|
||||
unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false
|
||||
pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
|
|
@ -810,4 +810,8 @@ const (
|
||||
// FlagUnifiedStorageSearch
|
||||
// Enable unified storage search
|
||||
FlagUnifiedStorageSearch = "unifiedStorageSearch"
|
||||
|
||||
// FlagPluginsSriChecks
|
||||
// Enables SRI checks for plugin assets
|
||||
FlagPluginsSriChecks = "pluginsSriChecks"
|
||||
)
|
||||
|
@ -2367,6 +2367,18 @@
|
||||
"codeowner": "@grafana/plugins-platform-backend"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "pluginsSriChecks",
|
||||
"resourceVersion": "1727785264632",
|
||||
"creationTimestamp": "2024-10-01T12:21:04Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables SRI checks for plugin assets",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/plugins-platform-backend"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "preserveDashboardStateWhenNavigating",
|
||||
|
@ -2,14 +2,21 @@ 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"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -21,18 +28,24 @@ var (
|
||||
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, cdn *pluginscdn.Service) *Service {
|
||||
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
cdn: cdn,
|
||||
log: log.New("pluginassets"),
|
||||
cfg: cfg,
|
||||
cdn: cdn,
|
||||
signature: sig,
|
||||
store: store,
|
||||
log: log.New("pluginassets"),
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg *setting.Cfg
|
||||
cdn *pluginscdn.Service
|
||||
log log.Logger
|
||||
cfg *config.PluginManagementCfg
|
||||
cdn *pluginscdn.Service
|
||||
signature *signature.Signature
|
||||
store pluginstore.Store
|
||||
log log.Logger
|
||||
|
||||
moduleHashCache sync.Map
|
||||
}
|
||||
|
||||
// LoadingStrategy calculates the loading strategy for a plugin.
|
||||
@ -69,6 +82,86 @@ 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.
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
@ -86,3 +179,17 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||
func (s *Service) cdnEnabled(pluginID string, class plugins.Class) bool {
|
||||
return s.cdn.PluginSupported(pluginID) || class == plugins.ClassCDN
|
||||
}
|
||||
|
||||
// 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,13 +2,17 @@ package pluginassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/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"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -34,7 +38,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: compatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
{
|
||||
@ -42,7 +46,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
|
||||
CreatePluginVersionCfgKey: compatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
@ -53,7 +57,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: futureVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
{
|
||||
@ -61,7 +65,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
// NOTE: cdn key is not set
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
{
|
||||
@ -70,7 +74,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
// NOTE: cdn key is not set
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Class = plugins.ClassExternal
|
||||
return p
|
||||
}),
|
||||
@ -83,7 +87,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
"cdn": "true",
|
||||
},
|
||||
},
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
@ -96,8 +100,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
"cdn": "true",
|
||||
},
|
||||
},
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Angular.Detected = true
|
||||
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
@ -106,8 +109,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
{
|
||||
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular",
|
||||
pluginSettings: setting.PluginSettings{},
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Angular.Detected = true
|
||||
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
@ -119,7 +121,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
"cdn": "true",
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Class = plugins.ClassExternal
|
||||
return p
|
||||
}),
|
||||
@ -130,7 +132,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, true),
|
||||
plugin: newPlugin(pluginID, withAngular(true)),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
{
|
||||
@ -139,7 +141,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
"cdn": "true",
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
{
|
||||
@ -147,7 +149,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Class = plugins.ClassCDN
|
||||
return p
|
||||
}),
|
||||
@ -158,7 +160,7 @@ func TestService_Calculate(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: "invalidSemver",
|
||||
}),
|
||||
plugin: newPlugin(pluginID, false),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
}
|
||||
@ -179,12 +181,305 @@ func TestService_Calculate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newPlugin(pluginID string, angular bool, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||
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
|
||||
plugin pluginstore.Plugin
|
||||
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: "",
|
||||
},
|
||||
{
|
||||
name: "feature flag on with cdn on should return module hash",
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||
},
|
||||
{
|
||||
name: "feature flag on with cdn off should return module hash",
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||
},
|
||||
{
|
||||
name: "feature flag off with cdn on should not return module hash",
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: false},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "feature flag off with cdn off should not return module hash",
|
||||
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: false,
|
||||
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: false,
|
||||
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: false,
|
||||
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: "",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var pluginSettings setting.PluginSettings
|
||||
if tc.cdn {
|
||||
pluginSettings = newPluginSettings(pluginID, 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: setting.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"))),
|
||||
)
|
||||
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{
|
||||
ID: pluginID,
|
||||
},
|
||||
Angular: plugins.AngularMeta{Detected: angular},
|
||||
}
|
||||
for _, cb := range cbs {
|
||||
p = cb(p)
|
||||
@ -192,8 +487,43 @@ func newPlugin(pluginID string, angular bool, cbs ...func(p pluginstore.Plugin)
|
||||
return p
|
||||
}
|
||||
|
||||
func newCfg(ps setting.PluginSettings) *setting.Cfg {
|
||||
return &setting.Cfg{
|
||||
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
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
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 newCfg(ps setting.PluginSettings) *config.PluginManagementCfg {
|
||||
return &config.PluginManagementCfg{
|
||||
PluginSettings: ps,
|
||||
}
|
||||
}
|
||||
@ -203,3 +533,9 @@ func newPluginSettings(pluginID string, kv map[string]string) setting.PluginSett
|
||||
pluginID: kv,
|
||||
}
|
||||
}
|
||||
|
||||
func newSRIHash(t *testing.T, s string) string {
|
||||
r, err := convertHashForSRI(s)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}
|
||||
|
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js
vendored
Normal file
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
hello parent
|
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json
vendored
Normal file
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Test",
|
||||
"id": "test-app",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
29
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt
vendored
Normal file
29
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1726230812215,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "31f04aceb2a9b14c2e501f38a4de5ab1c7a3e7306f58353fa5c1a86b716c971c",
|
||||
"something.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.11
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wrkEARMKAAYFAmbkMRwAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
|
||||
cIhm53UWAgkBE2oxqyzBji86eCOzLmCT7IgQaoSMMF48tu+XdgwFS5/NU5su
|
||||
deKad3taDnSU9a7GkCaisRVQOWy/UtFS1FNQTtkCCQBc1cZ6JsPWh2Pd60h0
|
||||
9U5aviYde6g1DCKO1riaUzHzrruBiHmHWjzr2aYwACb89vs2XcZqvue1Byb+
|
||||
y2inBDhHvQ==
|
||||
=qMej
|
||||
-----END PGP SIGNATURE-----
|
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json
vendored
Normal file
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Test",
|
||||
"id": "test-app",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js
vendored
Normal file
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
hello parent
|
@ -0,0 +1,33 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1726234125061,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711",
|
||||
"datasource/plugin.json": "3fd712717a21617cc76f9043efcd43d4ebf5564dd155a28e4e3c736739f6931e",
|
||||
"datasource/panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f",
|
||||
"datasource/panels/one/plugin.json": "b9b4556a7220ea77650ffd228da6d441e68df3405d50dab5773c10f4afae5ad3",
|
||||
"module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a",
|
||||
"plugin.json": "31f04aceb2a9b14c2e501f38a4de5ab1c7a3e7306f58353fa5c1a86b716c971c"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.11
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wrkEARMKAAYFAmbkPg0AIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
|
||||
cIhm5xTlAgkB3mG37KEdlP34nC69NbmriMpDH6PyyJ0IUwXB/SMTr4Gc2SvG
|
||||
cVHvih/0WqVjYKxxQI0QHoYpBQW2jPx0YJLFof8CCQBHpdEEXNTYOOZWG6Cg
|
||||
M3wB3AdCO+ChjXkKosbWqiMDfVqHFoLoLurwWxwOjvk/xTvX5GFbOxSfISyU
|
||||
8iW03F5/Sw==
|
||||
=wobV
|
||||
-----END PGP SIGNATURE-----
|
@ -0,0 +1 @@
|
||||
hello datasource
|
@ -0,0 +1 @@
|
||||
hello panel
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Test Panel",
|
||||
"id": "test-panel",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Test Datasource",
|
||||
"id": "test-datasource",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
hello parent
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Test",
|
||||
"id": "test-app",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
33
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt
vendored
Normal file
33
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1726230803822,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a",
|
||||
"plugin.json": "31f04aceb2a9b14c2e501f38a4de5ab1c7a3e7306f58353fa5c1a86b716c971c",
|
||||
"datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711",
|
||||
"datasource/plugin.json": "3fd712717a21617cc76f9043efcd43d4ebf5564dd155a28e4e3c736739f6931e",
|
||||
"panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f",
|
||||
"panels/one/plugin.json": "b9b4556a7220ea77650ffd228da6d441e68df3405d50dab5773c10f4afae5ad3"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.11
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wrkEARMKAAYFAmbkMRQAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
|
||||
cIhm50C8AgkAmzQpeYPnCgYimLGp5UGnCTrkbUEEqW+qXESrhi5T5ZuM+SzT
|
||||
BcRlC5pP6+wuyXAIdfppzWQ/umkkoaTIuub0TXQCCQHVcpWKy4acRL9TlORQ
|
||||
1VzVEV9PW0+x606HsDDHkterKQZgr5X6I/sTbSpBDMWPCMxqAk9fZn3G4iuq
|
||||
MyS+hwUZDQ==
|
||||
=7/Rd
|
||||
-----END PGP SIGNATURE-----
|
@ -0,0 +1 @@
|
||||
hello datasource
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Test Datasource",
|
||||
"id": "test-datasource",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js
vendored
Normal file
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
hello parent
|
@ -0,0 +1 @@
|
||||
hello panel
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Test Panel",
|
||||
"id": "test-panel",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json
vendored
Normal file
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "app",
|
||||
"name": "Test",
|
||||
"id": "test-app",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
32
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt
vendored
Normal file
32
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "private",
|
||||
"signedByOrg": "giuseppeguerra",
|
||||
"signedByOrgName": "giuseppeguerra",
|
||||
"rootUrls": [
|
||||
"http://127.0.0.1:3000/"
|
||||
],
|
||||
"plugin": "test-datasource",
|
||||
"version": "1.0.0",
|
||||
"time": 1725959570435,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
|
||||
"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.11
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wrkEARMKAAYFAmbgDZIAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
|
||||
cIhm5wbfAgkAXmKJcM8uAKb3TepYW/oyGhRLR8L6eM9mCoYwKkatITKJ6bRe
|
||||
Wnz37AMcPx0DahgfCzCXRLo4CspPJylr2JV8DagCCQCfCjHgLFhKGpBP71Y1
|
||||
mgcQ1/CJefb6B2H45G25MwUFTlSTGLDqW4QMi2kQvXnnUMjXquv2+iVd6qyz
|
||||
0Rqvpou/QQ==
|
||||
=QNmr
|
||||
-----END PGP SIGNATURE-----
|
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js
vendored
Normal file
1
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
hello
|
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json
vendored
Normal file
15
pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Test",
|
||||
"id": "test-datasource",
|
||||
"backend": true,
|
||||
"executable": "test",
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Giuseppe Guerra"
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro
|
||||
config.Features{
|
||||
ExternalCorePluginsEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins),
|
||||
SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars),
|
||||
SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks),
|
||||
},
|
||||
cfg.AngularSupportEnabled,
|
||||
cfg.GrafanaComAPIURL,
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
type Plugin struct {
|
||||
plugins.JSONData
|
||||
|
||||
fs plugins.FS
|
||||
FS plugins.FS
|
||||
supportsStreaming bool
|
||||
|
||||
Class plugins.Class
|
||||
@ -42,7 +42,7 @@ func (p Plugin) SupportsStreaming() bool {
|
||||
}
|
||||
|
||||
func (p Plugin) Base() string {
|
||||
return p.fs.Base()
|
||||
return p.FS.Base()
|
||||
}
|
||||
|
||||
func (p Plugin) IsApp() bool {
|
||||
@ -61,7 +61,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
||||
}
|
||||
|
||||
dto := Plugin{
|
||||
fs: p.FS,
|
||||
FS: p.FS,
|
||||
supportsStreaming: supportsStreaming,
|
||||
Class: p.Class,
|
||||
JSONData: p.JSONData,
|
||||
|
@ -63,6 +63,7 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
||||
isAngular: meta.angular?.detected,
|
||||
loadingStrategy: fallbackLoadingStrategy,
|
||||
pluginId: meta.id,
|
||||
moduleHash: meta.moduleHash,
|
||||
})
|
||||
.then((pluginExports) => {
|
||||
if (pluginExports.plugin) {
|
||||
|
@ -57,6 +57,7 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
||||
isAngular: config.angular.detected,
|
||||
pluginId,
|
||||
loadingStrategy,
|
||||
moduleHash: config.moduleHash,
|
||||
});
|
||||
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
||||
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
PluginLoadingStrategy,
|
||||
PluginMeta,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||
@ -73,12 +74,14 @@ export async function importPluginModule({
|
||||
loadingStrategy,
|
||||
version,
|
||||
isAngular,
|
||||
moduleHash,
|
||||
}: {
|
||||
path: string;
|
||||
pluginId: string;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
version?: string;
|
||||
isAngular?: boolean;
|
||||
moduleHash?: string;
|
||||
}): Promise<System.Module> {
|
||||
if (version) {
|
||||
registerPluginInCache({ path, version, loadingStrategy });
|
||||
@ -94,7 +97,21 @@ export async function importPluginModule({
|
||||
}
|
||||
}
|
||||
|
||||
let modulePath = resolveModulePath(path);
|
||||
const modulePath = resolveModulePath(path);
|
||||
|
||||
// inject integrity hash into SystemJS import map
|
||||
if (config.featureToggles.pluginsSriChecks) {
|
||||
const resolvedModule = System.resolve(modulePath);
|
||||
const integrityMap = System.getImportMap().integrity;
|
||||
|
||||
if (moduleHash && integrityMap && !integrityMap[resolvedModule]) {
|
||||
SystemJS.addImportMap({
|
||||
integrity: {
|
||||
[resolvedModule]: moduleHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// the sandboxing environment code cannot work in nodejs and requires a real browser
|
||||
if (await isFrontendSandboxSupported({ isAngular, pluginId })) {
|
||||
@ -113,6 +130,7 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
||||
isAngular,
|
||||
loadingStrategy: fallbackLoadingStrategy,
|
||||
pluginId: meta.id,
|
||||
moduleHash: meta.moduleHash,
|
||||
}).then((pluginExports) => {
|
||||
if (pluginExports.plugin) {
|
||||
const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin;
|
||||
@ -144,6 +162,7 @@ export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
||||
isAngular,
|
||||
loadingStrategy: fallbackLoadingStrategy,
|
||||
pluginId: meta.id,
|
||||
moduleHash: meta.moduleHash,
|
||||
}).then((pluginExports) => {
|
||||
const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin();
|
||||
plugin.init(meta);
|
||||
|
Loading…
Reference in New Issue
Block a user