mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 21:02:47 +08: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:
@ -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 |
|
| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time |
|
||||||
| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions |
|
| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions |
|
||||||
| `rolePickerDrawer` | Enables the new role picker drawer design |
|
| `rolePickerDrawer` | Enables the new role picker drawer design |
|
||||||
|
| `pluginsSriChecks` | Enables SRI checks for plugin assets |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
@ -219,4 +219,5 @@ export interface FeatureToggles {
|
|||||||
useSessionStorageForRedirection?: boolean;
|
useSessionStorageForRedirection?: boolean;
|
||||||
rolePickerDrawer?: boolean;
|
rolePickerDrawer?: boolean;
|
||||||
unifiedStorageSearch?: boolean;
|
unifiedStorageSearch?: boolean;
|
||||||
|
pluginsSriChecks?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
|||||||
angularDetected?: boolean;
|
angularDetected?: boolean;
|
||||||
loadingStrategy?: PluginLoadingStrategy;
|
loadingStrategy?: PluginLoadingStrategy;
|
||||||
extensions?: PluginExtensions;
|
extensions?: PluginExtensions;
|
||||||
|
moduleHash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginDependencyInfo {
|
interface PluginDependencyInfo {
|
||||||
|
@ -46,6 +46,7 @@ export type AppPluginConfig = {
|
|||||||
loadingStrategy: PluginLoadingStrategy;
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
dependencies: PluginDependencies;
|
dependencies: PluginDependencies;
|
||||||
extensions: PluginExtensions;
|
extensions: PluginExtensions;
|
||||||
|
moduleHash?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreinstalledPlugin = {
|
export type PreinstalledPlugin = {
|
||||||
|
@ -30,6 +30,7 @@ type PluginSetting struct {
|
|||||||
SignatureOrg string `json:"signatureOrg"`
|
SignatureOrg string `json:"signatureOrg"`
|
||||||
AngularDetected bool `json:"angularDetected"`
|
AngularDetected bool `json:"angularDetected"`
|
||||||
LoadingStrategy plugins.LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy plugins.LoadingStrategy `json:"loadingStrategy"`
|
||||||
|
ModuleHash string `json:"moduleHash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginListItem struct {
|
type PluginListItem struct {
|
||||||
|
@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
|||||||
AliasIDs: panel.AliasIDs,
|
AliasIDs: panel.AliasIDs,
|
||||||
Info: panel.Info,
|
Info: panel.Info,
|
||||||
Module: panel.Module,
|
Module: panel.Module,
|
||||||
|
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel),
|
||||||
BaseURL: panel.BaseURL,
|
BaseURL: panel.BaseURL,
|
||||||
SkipDataQuery: panel.SkipDataQuery,
|
SkipDataQuery: panel.SkipDataQuery,
|
||||||
HideFromList: panel.HideFromList,
|
HideFromList: panel.HideFromList,
|
||||||
@ -453,6 +454,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
|||||||
JSONData: plugin.JSONData,
|
JSONData: plugin.JSONData,
|
||||||
Signature: plugin.Signature,
|
Signature: plugin.Signature,
|
||||||
Module: plugin.Module,
|
Module: plugin.Module,
|
||||||
|
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin),
|
||||||
BaseURL: plugin.BaseURL,
|
BaseURL: plugin.BaseURL,
|
||||||
Angular: plugin.Angular,
|
Angular: plugin.Angular,
|
||||||
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||||
@ -538,8 +540,9 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
|||||||
JSONData: ds.JSONData,
|
JSONData: ds.JSONData,
|
||||||
Signature: ds.Signature,
|
Signature: ds.Signature,
|
||||||
Module: ds.Module,
|
Module: ds.Module,
|
||||||
BaseURL: ds.BaseURL,
|
// ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), ds),
|
||||||
Angular: ds.Angular,
|
BaseURL: ds.BaseURL,
|
||||||
|
Angular: ds.Angular,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if ds.Name == grafanads.DatasourceName {
|
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),
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||||
Extensions: plugin.Extensions,
|
Extensions: plugin.Extensions,
|
||||||
Dependencies: plugin.Dependencies,
|
Dependencies: plugin.Dependencies,
|
||||||
|
ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin),
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Enabled {
|
if settings.Enabled {
|
||||||
|
@ -18,6 +18,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/login/social/socialimpl"
|
"github.com/grafana/grafana/pkg/login/social/socialimpl"
|
||||||
"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/signature"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
"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"
|
||||||
@ -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,
|
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
||||||
PluginSettings: cfg.PluginSettings,
|
PluginSettings: cfg.PluginSettings,
|
||||||
})
|
}
|
||||||
|
pluginsCDN := pluginscdn.ProvideService(pluginsCfg)
|
||||||
|
|
||||||
var pluginStore = pstore
|
var pluginStore = pstore
|
||||||
if pluginStore == nil {
|
if pluginStore == nil {
|
||||||
@ -68,7 +71,8 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
|
|
||||||
var pluginsAssets = passets
|
var pluginsAssets = passets
|
||||||
if pluginsAssets == nil {
|
if pluginsAssets == nil {
|
||||||
pluginsAssets = pluginassets.ProvideService(cfg, pluginsCDN)
|
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
||||||
|
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
@ -240,6 +244,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
PluginList: []pluginstore.Plugin{
|
PluginList: []pluginstore.Plugin{
|
||||||
{
|
{
|
||||||
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||||
|
// ModuleHash: "sha256-test",
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
ID: "test-app",
|
ID: "test-app",
|
||||||
Info: plugins.Info{Version: "0.5.0"},
|
Info: plugins.Info{Version: "0.5.0"},
|
||||||
@ -255,9 +260,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", false),
|
Plugins: newAppSettings("test-app", false),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginAssets: func() *pluginassets.Service {
|
pluginAssets: newPluginAssets(),
|
||||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
|
||||||
},
|
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
@ -266,6 +269,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Path: "/test-app/module.js",
|
Path: "/test-app/module.js",
|
||||||
Version: "0.5.0",
|
Version: "0.5.0",
|
||||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
|
// ModuleHash: "sha256-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -277,6 +281,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
PluginList: []pluginstore.Plugin{
|
PluginList: []pluginstore.Plugin{
|
||||||
{
|
{
|
||||||
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||||
|
// ModuleHash: "sha256-test",
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
ID: "test-app",
|
ID: "test-app",
|
||||||
Info: plugins.Info{Version: "0.5.0"},
|
Info: plugins.Info{Version: "0.5.0"},
|
||||||
@ -292,9 +297,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginAssets: func() *pluginassets.Service {
|
pluginAssets: newPluginAssets(),
|
||||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
|
||||||
},
|
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
@ -303,6 +306,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Path: "/test-app/module.js",
|
Path: "/test-app/module.js",
|
||||||
Version: "0.5.0",
|
Version: "0.5.0",
|
||||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
|
// ModuleHash: "sha256-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -330,9 +334,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginAssets: func() *pluginassets.Service {
|
pluginAssets: newPluginAssets(),
|
||||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
|
||||||
},
|
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
@ -368,15 +370,13 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginAssets: func() *pluginassets.Service {
|
pluginAssets: newPluginAssetsWithConfig(&config.PluginManagementCfg{
|
||||||
return pluginassets.ProvideService(&setting.Cfg{
|
PluginSettings: map[string]map[string]string{
|
||||||
PluginSettings: map[string]map[string]string{
|
"test-app": {
|
||||||
"test-app": {
|
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
|
||||||
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}, pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
},
|
||||||
},
|
}),
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
@ -412,9 +412,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pluginAssets: func() *pluginassets.Service {
|
pluginAssets: newPluginAssets(),
|
||||||
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
|
||||||
},
|
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"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,
|
Includes: plugin.Includes,
|
||||||
BaseUrl: plugin.BaseURL,
|
BaseUrl: plugin.BaseURL,
|
||||||
Module: plugin.Module,
|
Module: plugin.Module,
|
||||||
|
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin),
|
||||||
DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
|
DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
|
||||||
State: plugin.State,
|
State: plugin.State,
|
||||||
Signature: plugin.Signature,
|
Signature: plugin.Signature,
|
||||||
|
@ -27,6 +27,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
|
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
|
||||||
"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/statickey"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||||
"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"
|
||||||
@ -788,7 +790,6 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
Info: plugins.Info{
|
Info: plugins.Info{
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
}}, plugins.ClassExternal, plugins.NewFakeFS())
|
}}, plugins.ClassExternal, plugins.NewFakeFS())
|
||||||
|
|
||||||
pluginRegistry := &fakes.FakePluginRegistry{
|
pluginRegistry := &fakes.FakePluginRegistry{
|
||||||
Store: map[string]*plugins.Plugin{
|
Store: map[string]*plugins.Plugin{
|
||||||
p1.ID: p1,
|
p1.ID: p1,
|
||||||
@ -843,8 +844,10 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
ErrorCode: tc.errCode,
|
ErrorCode: tc.errCode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pluginCDN := pluginscdn.ProvideService(&config.PluginManagementCfg{})
|
pCfg := &config.PluginManagementCfg{}
|
||||||
hs.pluginAssets = pluginassets.ProvideService(hs.Cfg, pluginCDN)
|
pluginCDN := pluginscdn.ProvideService(pCfg)
|
||||||
|
sig := signature.ProvideService(pCfg, statickey.New())
|
||||||
|
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore)
|
||||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||||
var err error
|
var err error
|
||||||
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
||||||
|
@ -32,6 +32,7 @@ type PluginManagementCfg struct {
|
|||||||
type Features struct {
|
type Features struct {
|
||||||
ExternalCorePluginsEnabled bool
|
ExternalCorePluginsEnabled bool
|
||||||
SkipHostEnvVarsEnabled bool
|
SkipHostEnvVarsEnabled bool
|
||||||
|
SriChecksEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginManagementCfg returns a new PluginManagementCfg.
|
// NewPluginManagementCfg returns a new PluginManagementCfg.
|
||||||
|
@ -53,7 +53,7 @@ type PluginManifest struct {
|
|||||||
RootURLs []string `json:"rootUrls"`
|
RootURLs []string `json:"rootUrls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *PluginManifest) isV2() bool {
|
func (m *PluginManifest) IsV2() bool {
|
||||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,34 +107,17 @@ func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*Plugi
|
|||||||
return &manifest, nil
|
return &manifest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned")
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 err != nil {
|
||||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||||
s.log.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
return nil, fmt.Errorf("%w: could not find a MANIFEST.txt", ErrSignatureTypeUnsigned)
|
||||||
return plugins.Signature{
|
|
||||||
Status: plugins.SignatureStatusUnsigned,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("could not open MANIFEST.txt: %w", err)
|
||||||
s.log.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "error", err)
|
|
||||||
return plugins.Signature{
|
|
||||||
Status: plugins.SignatureStatusInvalid,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if f == nil {
|
if f == nil {
|
||||||
@ -147,21 +130,47 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
|
|
||||||
byteValue, err := io.ReadAll(f)
|
byteValue, err := io.ReadAll(f)
|
||||||
if err != nil || len(byteValue) < 10 {
|
if err != nil || len(byteValue) < 10 {
|
||||||
s.log.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
return nil, fmt.Errorf("%w: MANIFEST.txt is invalid", ErrSignatureTypeUnsigned)
|
||||||
return plugins.Signature{
|
|
||||||
Status: plugins.SignatureStatusUnsigned,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest, err := s.readPluginManifest(ctx, byteValue)
|
manifest, err := s.readPluginManifest(ctx, byteValue)
|
||||||
if err != nil {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureStatusInvalid,
|
Status: plugins.SignatureStatusInvalid,
|
||||||
}, nil
|
}, 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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureStatusInvalid,
|
Status: plugins.SignatureStatusInvalid,
|
||||||
}, nil
|
}, nil
|
||||||
@ -328,7 +337,7 @@ func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, bloc
|
|||||||
if len(m.Files) == 0 {
|
if len(m.Files) == 0 {
|
||||||
return invalidFieldErr{field: "files"}
|
return invalidFieldErr{field: "files"}
|
||||||
}
|
}
|
||||||
if m.isV2() {
|
if m.IsV2() {
|
||||||
if len(m.SignedByOrg) == 0 {
|
if len(m.SignedByOrg) == 0 {
|
||||||
return invalidFieldErr{field: "signedByOrg"}
|
return invalidFieldErr{field: "signedByOrg"}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
"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) {
|
func TestReadPluginManifest(t *testing.T) {
|
||||||
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
Hash: SHA512
|
Hash: SHA512
|
||||||
@ -52,7 +60,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
|||||||
-----END PGP SIGNATURE-----`
|
-----END PGP SIGNATURE-----`
|
||||||
|
|
||||||
t.Run("valid manifest", func(t *testing.T) {
|
t.Run("valid manifest", func(t *testing.T) {
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
s := provideDefaultTestService()
|
||||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -68,8 +76,8 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid manifest", func(t *testing.T) {
|
t.Run("invalid manifest", func(t *testing.T) {
|
||||||
|
s := provideDefaultTestService()
|
||||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
||||||
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
@ -107,7 +115,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
|||||||
-----END PGP SIGNATURE-----`
|
-----END PGP SIGNATURE-----`
|
||||||
|
|
||||||
t.Run("valid manifest", func(t *testing.T) {
|
t.Run("valid manifest", func(t *testing.T) {
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
s := provideDefaultTestService()
|
||||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -126,6 +134,12 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculate(t *testing.T) {
|
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) {
|
t.Run("Validate root URL against App URL for non-private plugin if is specified in manifest", func(t *testing.T) {
|
||||||
tcs := []struct {
|
tcs := []struct {
|
||||||
appURL string
|
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 {
|
for _, tc := range tcs {
|
||||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
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{
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||||
return plugins.ClassExternal
|
return plugins.ClassExternal
|
||||||
@ -183,7 +191,7 @@ func TestCalculate(t *testing.T) {
|
|||||||
basePath := "../testdata/renderer-added-file/plugin"
|
basePath := "../testdata/renderer-added-file/plugin"
|
||||||
|
|
||||||
runningWindows = true
|
runningWindows = true
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
s := provideDefaultTestService()
|
||||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||||
return plugins.ClassExternal
|
return plugins.ClassExternal
|
||||||
@ -247,7 +255,7 @@ func TestCalculate(t *testing.T) {
|
|||||||
toSlash = tc.platform.toSlashFunc()
|
toSlash = tc.platform.toSlashFunc()
|
||||||
fromSlash = tc.platform.fromSlashFunc()
|
fromSlash = tc.platform.fromSlashFunc()
|
||||||
|
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
s := provideDefaultTestService()
|
||||||
pfs, err := tc.fsFactory()
|
pfs, err := tc.fsFactory()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||||
@ -721,7 +729,7 @@ func Test_validateManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
s := provideDefaultTestService()
|
||||||
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
||||||
require.Errorf(t, err, tc.expectedErr)
|
require.Errorf(t, err, tc.expectedErr)
|
||||||
})
|
})
|
||||||
|
@ -262,6 +262,7 @@ type PluginMetaDTO struct {
|
|||||||
JSONData
|
JSONData
|
||||||
Signature SignatureStatus `json:"signature"`
|
Signature SignatureStatus `json:"signature"`
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
|
ModuleHash string `json:"moduleHash,omitempty"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||||
@ -314,6 +315,7 @@ type PanelDTO struct {
|
|||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
|
ModuleHash string `json:"moduleHash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppDTO struct {
|
type AppDTO struct {
|
||||||
@ -325,6 +327,7 @@ type AppDTO struct {
|
|||||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
Extensions Extensions `json:"extensions"`
|
Extensions Extensions `json:"extensions"`
|
||||||
Dependencies Dependencies `json:"dependencies"`
|
Dependencies Dependencies `json:"dependencies"`
|
||||||
|
ModuleHash string `json:"moduleHash,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1514,6 +1514,12 @@ var (
|
|||||||
HideFromDocs: true,
|
HideFromDocs: true,
|
||||||
HideFromAdminPage: 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
|
useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,false,false
|
||||||
rolePickerDrawer,experimental,@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
|
unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false
|
||||||
|
pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
|
|
@ -810,4 +810,8 @@ const (
|
|||||||
// FlagUnifiedStorageSearch
|
// FlagUnifiedStorageSearch
|
||||||
// Enable unified storage search
|
// Enable unified storage search
|
||||||
FlagUnifiedStorageSearch = "unifiedStorageSearch"
|
FlagUnifiedStorageSearch = "unifiedStorageSearch"
|
||||||
|
|
||||||
|
// FlagPluginsSriChecks
|
||||||
|
// Enables SRI checks for plugin assets
|
||||||
|
FlagPluginsSriChecks = "pluginsSriChecks"
|
||||||
)
|
)
|
||||||
|
@ -2367,6 +2367,18 @@
|
|||||||
"codeowner": "@grafana/plugins-platform-backend"
|
"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": {
|
"metadata": {
|
||||||
"name": "preserveDashboardStateWhenNavigating",
|
"name": "preserveDashboardStateWhenNavigating",
|
||||||
|
@ -2,14 +2,21 @@ 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/manager/signature"
|
||||||
"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"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,18 +28,24 @@ var (
|
|||||||
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
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{
|
return &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cdn: cdn,
|
cdn: cdn,
|
||||||
log: log.New("pluginassets"),
|
signature: sig,
|
||||||
|
store: store,
|
||||||
|
log: log.New("pluginassets"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg *setting.Cfg
|
cfg *config.PluginManagementCfg
|
||||||
cdn *pluginscdn.Service
|
cdn *pluginscdn.Service
|
||||||
log log.Logger
|
signature *signature.Signature
|
||||||
|
store pluginstore.Store
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
moduleHashCache sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadingStrategy calculates the loading strategy for a plugin.
|
// 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
|
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 {
|
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||||
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
|
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
|
||||||
createPluginVer, err := semver.NewVersion(cpv)
|
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 {
|
func (s *Service) cdnEnabled(pluginID string, class plugins.Class) bool {
|
||||||
return s.cdn.PluginSupported(pluginID) || class == plugins.ClassCDN
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"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/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"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -34,7 +38,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
CreatePluginVersionCfgKey: compatVersion,
|
CreatePluginVersionCfgKey: compatVersion,
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, false),
|
plugin: newPlugin(pluginID, withAngular(false)),
|
||||||
expected: plugins.LoadingStrategyScript,
|
expected: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -42,7 +46,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
|
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
|
||||||
CreatePluginVersionCfgKey: compatVersion,
|
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"}
|
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -53,7 +57,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
CreatePluginVersionCfgKey: futureVersion,
|
CreatePluginVersionCfgKey: futureVersion,
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, false),
|
plugin: newPlugin(pluginID, withAngular(false)),
|
||||||
expected: plugins.LoadingStrategyScript,
|
expected: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61,7 +65,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
// NOTE: cdn key is not set
|
// NOTE: cdn key is not set
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, false),
|
plugin: newPlugin(pluginID, withAngular(false)),
|
||||||
expected: plugins.LoadingStrategyScript,
|
expected: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,7 +74,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
CreatePluginVersionCfgKey: incompatVersion,
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
// NOTE: cdn key is not set
|
// 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
|
p.Class = plugins.ClassExternal
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -83,7 +87,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
"cdn": "true",
|
"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"}
|
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -96,8 +100,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
"cdn": "true",
|
"cdn": "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Angular.Detected = true
|
|
||||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||||
return p
|
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",
|
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular",
|
||||||
pluginSettings: setting.PluginSettings{},
|
pluginSettings: setting.PluginSettings{},
|
||||||
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
p.Angular.Detected = true
|
|
||||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -119,7 +121,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
"cdn": "true",
|
"cdn": "true",
|
||||||
CreatePluginVersionCfgKey: incompatVersion,
|
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
|
p.Class = plugins.ClassExternal
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -130,7 +132,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
CreatePluginVersionCfgKey: incompatVersion,
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, true),
|
plugin: newPlugin(pluginID, withAngular(true)),
|
||||||
expected: plugins.LoadingStrategyFetch,
|
expected: plugins.LoadingStrategyFetch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -139,7 +141,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
"cdn": "true",
|
"cdn": "true",
|
||||||
CreatePluginVersionCfgKey: incompatVersion,
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, false),
|
plugin: newPlugin(pluginID, withAngular(false)),
|
||||||
expected: plugins.LoadingStrategyFetch,
|
expected: plugins.LoadingStrategyFetch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -147,7 +149,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
CreatePluginVersionCfgKey: incompatVersion,
|
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
|
p.Class = plugins.ClassCDN
|
||||||
return p
|
return p
|
||||||
}),
|
}),
|
||||||
@ -158,7 +160,7 @@ func TestService_Calculate(t *testing.T) {
|
|||||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
CreatePluginVersionCfgKey: "invalidSemver",
|
CreatePluginVersionCfgKey: "invalidSemver",
|
||||||
}),
|
}),
|
||||||
plugin: newPlugin(pluginID, false),
|
plugin: newPlugin(pluginID, withAngular(false)),
|
||||||
expected: plugins.LoadingStrategyScript,
|
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{
|
p := pluginstore.Plugin{
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
ID: pluginID,
|
ID: pluginID,
|
||||||
},
|
},
|
||||||
Angular: plugins.AngularMeta{Detected: angular},
|
|
||||||
}
|
}
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
p = cb(p)
|
p = cb(p)
|
||||||
@ -192,8 +487,43 @@ func newPlugin(pluginID string, angular bool, cbs ...func(p pluginstore.Plugin)
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCfg(ps setting.PluginSettings) *setting.Cfg {
|
func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
return &setting.Cfg{
|
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,
|
PluginSettings: ps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,3 +533,9 @@ func newPluginSettings(pluginID string, kv map[string]string) setting.PluginSett
|
|||||||
pluginID: kv,
|
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{
|
config.Features{
|
||||||
ExternalCorePluginsEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins),
|
ExternalCorePluginsEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalCorePlugins),
|
||||||
SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars),
|
SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars),
|
||||||
|
SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks),
|
||||||
},
|
},
|
||||||
cfg.AngularSupportEnabled,
|
cfg.AngularSupportEnabled,
|
||||||
cfg.GrafanaComAPIURL,
|
cfg.GrafanaComAPIURL,
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
plugins.JSONData
|
plugins.JSONData
|
||||||
|
|
||||||
fs plugins.FS
|
FS plugins.FS
|
||||||
supportsStreaming bool
|
supportsStreaming bool
|
||||||
|
|
||||||
Class plugins.Class
|
Class plugins.Class
|
||||||
@ -42,7 +42,7 @@ func (p Plugin) SupportsStreaming() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p Plugin) Base() string {
|
func (p Plugin) Base() string {
|
||||||
return p.fs.Base()
|
return p.FS.Base()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Plugin) IsApp() bool {
|
func (p Plugin) IsApp() bool {
|
||||||
@ -61,7 +61,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dto := Plugin{
|
dto := Plugin{
|
||||||
fs: p.FS,
|
FS: p.FS,
|
||||||
supportsStreaming: supportsStreaming,
|
supportsStreaming: supportsStreaming,
|
||||||
Class: p.Class,
|
Class: p.Class,
|
||||||
JSONData: p.JSONData,
|
JSONData: p.JSONData,
|
||||||
|
@ -63,6 +63,7 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
|||||||
isAngular: meta.angular?.detected,
|
isAngular: meta.angular?.detected,
|
||||||
loadingStrategy: fallbackLoadingStrategy,
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
|
moduleHash: meta.moduleHash,
|
||||||
})
|
})
|
||||||
.then((pluginExports) => {
|
.then((pluginExports) => {
|
||||||
if (pluginExports.plugin) {
|
if (pluginExports.plugin) {
|
||||||
|
@ -57,6 +57,7 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
|||||||
isAngular: config.angular.detected,
|
isAngular: config.angular.detected,
|
||||||
pluginId,
|
pluginId,
|
||||||
loadingStrategy,
|
loadingStrategy,
|
||||||
|
moduleHash: config.moduleHash,
|
||||||
});
|
});
|
||||||
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
PluginLoadingStrategy,
|
PluginLoadingStrategy,
|
||||||
PluginMeta,
|
PluginMeta,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
|
||||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||||
@ -73,12 +74,14 @@ export async function importPluginModule({
|
|||||||
loadingStrategy,
|
loadingStrategy,
|
||||||
version,
|
version,
|
||||||
isAngular,
|
isAngular,
|
||||||
|
moduleHash,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
loadingStrategy: PluginLoadingStrategy;
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
version?: string;
|
version?: string;
|
||||||
isAngular?: boolean;
|
isAngular?: boolean;
|
||||||
|
moduleHash?: string;
|
||||||
}): Promise<System.Module> {
|
}): Promise<System.Module> {
|
||||||
if (version) {
|
if (version) {
|
||||||
registerPluginInCache({ path, version, loadingStrategy });
|
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
|
// the sandboxing environment code cannot work in nodejs and requires a real browser
|
||||||
if (await isFrontendSandboxSupported({ isAngular, pluginId })) {
|
if (await isFrontendSandboxSupported({ isAngular, pluginId })) {
|
||||||
@ -113,6 +130,7 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
|||||||
isAngular,
|
isAngular,
|
||||||
loadingStrategy: fallbackLoadingStrategy,
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
|
moduleHash: meta.moduleHash,
|
||||||
}).then((pluginExports) => {
|
}).then((pluginExports) => {
|
||||||
if (pluginExports.plugin) {
|
if (pluginExports.plugin) {
|
||||||
const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin;
|
const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin;
|
||||||
@ -144,6 +162,7 @@ export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
|||||||
isAngular,
|
isAngular,
|
||||||
loadingStrategy: fallbackLoadingStrategy,
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
|
moduleHash: meta.moduleHash,
|
||||||
}).then((pluginExports) => {
|
}).then((pluginExports) => {
|
||||||
const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin();
|
const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin();
|
||||||
plugin.init(meta);
|
plugin.init(meta);
|
||||||
|
Reference in New Issue
Block a user