@ -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
}