Plugins: Fs: Add option to access unallowed files in dev mode (#66492)

* Plugins: Fs: Add option to access unallowed files in dev mode

* Plugins: Fs: allow accessing unallowed files only when in dev mode

* Plugins: Fs: Add ProvideLocalFinder

* Plugins: FS: Pass whole config in NewLocalFinder()

* Plugins: FS: Add AllowListLocalFS

* Plugins: FS: Fix some tests

* Plugins: FS: Update tests

* Plugins: FS: Removed dead code

* Plugins: FS: Add tests for AllowListFS

* Plugins: FS: Update comments

* Plugins: FS: Use variadic arguments for allow list rather than map

* Plugins: FS: Remove unnecessary log

* Plugins: FS: Do not escape plugin root dir

* Fix merge conflict

* Plugins: FS: Update comments

* Plugins: FS: PR review changes

* Fix merge conflict

* Fix tests

* Cleanup

* Fix flaky test

* Changes from PR review

* Lint

* Add comment to LocalFS.Remove

* Fix Windows

* Renamed devMode to production
This commit is contained in:
Giuseppe Guerra
2023-04-27 10:26:15 +02:00
committed by GitHub
parent dfc99cdd19
commit bfe1b68800
14 changed files with 576 additions and 366 deletions
+2 -2
View File
@@ -364,8 +364,8 @@ func (f *FakePluginFiles) Base() string {
return f.base
}
func (f *FakePluginFiles) Files() []string {
return []string{}
func (f *FakePluginFiles) Files() ([]string, error) {
return []string{}, nil
}
func (f *FakePluginFiles) Remove() error {
+21 -63
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/util"
)
@@ -24,15 +25,21 @@ var (
)
type Local struct {
log log.Logger
log log.Logger
production bool
}
func NewLocalFinder() *Local {
func NewLocalFinder(cfg *config.Cfg) *Local {
return &Local{
log: log.New("local.finder"),
production: !cfg.DevMode,
log: log.New("local.finder"),
}
}
func ProvideLocalFinder(cfg *config.Cfg) *Local {
return NewLocalFinder(cfg)
}
func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) {
if len(src.PluginURIs(ctx)) == 0 {
return []*plugins.FoundBundle{}, nil
@@ -81,15 +88,21 @@ func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.
var res = make(map[string]*plugins.FoundBundle)
for pluginDir, data := range foundPlugins {
files, err := collectFilesWithin(pluginDir)
if err != nil {
return nil, err
var pluginFs plugins.FS
pluginFs = plugins.NewLocalFS(pluginDir)
if l.production {
// In prod, tighten up security by allowing access only to the files present up to this point.
// Any new file "sneaked in" won't be allowed and will acts as if the file did not exist.
var err error
pluginFs, err = plugins.NewStaticFS(pluginFs)
if err != nil {
return nil, err
}
}
res[pluginDir] = &plugins.FoundBundle{
Primary: plugins.FoundPlugin{
JSONData: data,
FS: plugins.NewLocalFS(files, pluginDir),
FS: pluginFs,
},
}
}
@@ -190,61 +203,6 @@ func (l *Local) getAbsPluginJSONPaths(path string) ([]string, error) {
return pluginJSONPaths, nil
}
func collectFilesWithin(dir string) (map[string]struct{}, error) {
files := map[string]struct{}{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
symlinkPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
symlink, err := os.Stat(symlinkPath)
if err != nil {
return err
}
// verify that symlinked file is within plugin directory
p, err := filepath.Rel(dir, symlinkPath)
if err != nil {
return err
}
if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) {
return fmt.Errorf("file '%s' not inside of plugin directory", p)
}
// skip adding symlinked directories
if symlink.IsDir() {
return nil
}
}
// skip directories
if info.IsDir() {
return nil
}
// verify that file is within plugin directory
file, err := filepath.Rel(dir, path)
if err != nil {
return err
}
if strings.HasPrefix(file, ".."+string(filepath.Separator)) {
return fmt.Errorf("file '%s' not inside of plugin directory", file)
}
files[path] = struct{}{}
return nil
})
return files, err
}
func (l *Local) readFile(pluginJSONPath string) (io.ReadCloser, error) {
l.log.Debug("Loading plugin", "path", pluginJSONPath)
+40 -45
View File
@@ -14,7 +14,10 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -23,6 +26,11 @@ func TestFinder_Find(t *testing.T) {
if err != nil {
require.NoError(t, err)
}
cfg := setting.NewCfg()
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
require.NoError(t, err)
testCases := []struct {
name string
pluginDirs []string
@@ -55,10 +63,7 @@ func TestFinder_Find(t *testing.T) {
Backend: true,
Executable: "test",
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "valid-v2-signature/plugin/plugin.json"): {},
filepath.Join(testData, "valid-v2-signature/plugin/MANIFEST.txt"): {},
}, filepath.Join(testData, "valid-v2-signature/plugin")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "valid-v2-signature/plugin")),
},
},
},
@@ -87,12 +92,7 @@ func TestFinder_Find(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "duplicate-plugins/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/MANIFEST.txt"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {},
}, filepath.Join(testData, "duplicate-plugins/nested")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
},
Children: []*plugins.FoundPlugin{
{
@@ -114,10 +114,7 @@ func TestFinder_Find(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {},
}, filepath.Join(testData, "duplicate-plugins/nested/nested")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
},
},
},
@@ -178,14 +175,7 @@ func TestFinder_Find(t *testing.T) {
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer"},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "includes-symlinks/MANIFEST.txt"): {},
filepath.Join(testData, "includes-symlinks/dashboards/connections.json"): {},
filepath.Join(testData, "includes-symlinks/dashboards/extra/memory.json"): {},
filepath.Join(testData, "includes-symlinks/plugin.json"): {},
filepath.Join(testData, "includes-symlinks/symlink_to_txt"): {},
filepath.Join(testData, "includes-symlinks/text.txt"): {},
}, filepath.Join(testData, "includes-symlinks")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "includes-symlinks")),
},
},
},
@@ -213,12 +203,7 @@ func TestFinder_Find(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "duplicate-plugins/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/MANIFEST.txt"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {},
}, filepath.Join(testData, "duplicate-plugins/nested")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
},
Children: []*plugins.FoundPlugin{
{
@@ -240,10 +225,7 @@ func TestFinder_Find(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {},
filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {},
}, filepath.Join(testData, "duplicate-plugins/nested/nested")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
},
},
},
@@ -267,10 +249,7 @@ func TestFinder_Find(t *testing.T) {
State: plugins.AlphaRelease,
Backend: true,
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(testData, "invalid-v1-signature/plugin/plugin.json"): {},
filepath.Join(testData, "invalid-v1-signature/plugin/MANIFEST.txt"): {},
}, filepath.Join(testData, "invalid-v1-signature/plugin")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "invalid-v1-signature/plugin")),
},
},
},
@@ -278,7 +257,7 @@ func TestFinder_Find(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := NewLocalFinder()
f := NewLocalFinder(pCfg)
pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{
PluginURIsFunc: func(ctx context.Context) []string {
return tc.pluginDirs
@@ -294,14 +273,18 @@ func TestFinder_Find(t *testing.T) {
return pluginBundles[i].Primary.JSONData.ID < pluginBundles[j].Primary.JSONData.ID
})
if !cmp.Equal(pluginBundles, tc.expectedBundles, localFSComparer) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(pluginBundles, tc.expectedBundles, localFSComparer))
if !cmp.Equal(pluginBundles, tc.expectedBundles, fsComparer) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(pluginBundles, tc.expectedBundles, fsComparer))
}
})
}
}
func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
cfg := setting.NewCfg()
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
require.NoError(t, err)
t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error {
@@ -311,7 +294,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := NewLocalFinder()
finder := NewLocalFinder(pCfg)
paths, err := finder.getAbsPluginJSONPaths("test")
require.NoError(t, err)
require.Empty(t, paths)
@@ -326,7 +309,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := NewLocalFinder()
finder := NewLocalFinder(pCfg)
paths, err := finder.getAbsPluginJSONPaths("test")
require.NoError(t, err)
require.Empty(t, paths)
@@ -341,7 +324,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := NewLocalFinder()
finder := NewLocalFinder(pCfg)
paths, err := finder.getAbsPluginJSONPaths("test")
require.Error(t, err)
require.Empty(t, paths)
@@ -469,9 +452,15 @@ func TestFinder_readPluginJSON(t *testing.T) {
}
}
var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS) bool {
fs1Files := fs1.Files()
fs2Files := fs2.Files()
var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool {
fs1Files, err := fs1.Files()
if err != nil {
panic(err)
}
fs2Files, err := fs2.Files()
if err != nil {
panic(err)
}
sort.SliceStable(fs1Files, func(i, j int) bool {
return fs1Files[i] < fs1Files[j]
@@ -483,3 +472,9 @@ var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS
return cmp.Equal(fs1Files, fs2Files) && fs1.Base() == fs2.Base()
})
func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
require.NoError(t, err)
return sfs
}
+58 -123
View File
@@ -3,7 +3,6 @@ package loader
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"testing"
@@ -28,13 +27,18 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log"), localFSComparer}
var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log"), fsComparer}
var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS) bool {
fs1Files := fs1.Files()
fs2Files := fs2.Files()
var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool {
fs1Files, err := fs1.Files()
if err != nil {
panic(err)
}
fs2Files, err := fs2.Files()
if err != nil {
panic(err)
}
finder.NewLocalFinder()
sort.SliceStable(fs1Files, func(i, j int) bool {
return fs1Files[i] < fs1Files[j]
})
@@ -108,9 +112,8 @@ func TestLoader_Load(t *testing.T) {
},
Module: "app/plugins/datasource/cloudwatch/module",
BaseURL: "public/app/plugins/datasource/cloudwatch",
FS: plugins.NewLocalFS(
filesInDir(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")),
filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")),
FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")),
Signature: plugins.SignatureInternal,
Class: plugins.Core,
},
@@ -147,12 +150,9 @@ func TestLoader_Load(t *testing.T) {
Backend: true,
State: "alpha",
},
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: plugins.NewLocalFS(
filesInDir(t, filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/")),
filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/"),
),
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/")),
Signature: "valid",
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
@@ -226,20 +226,10 @@ func TestLoader_Load(t *testing.T) {
},
},
},
Class: plugins.External,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
FS: plugins.NewLocalFS(
map[string]struct{}{
filepath.Join(parentDir, "testdata/includes-symlinks", "/MANIFEST.txt"): {},
filepath.Join(parentDir, "testdata/includes-symlinks", "dashboards/connections.json"): {},
filepath.Join(parentDir, "testdata/includes-symlinks", "dashboards/extra/memory.json"): {},
filepath.Join(parentDir, "testdata/includes-symlinks", "plugin.json"): {},
filepath.Join(parentDir, "testdata/includes-symlinks", "symlink_to_txt"): {},
filepath.Join(parentDir, "testdata/includes-symlinks", "text.txt"): {},
},
filepath.Join(parentDir, "testdata/includes-symlinks"),
),
Class: plugins.External,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")),
Signature: "valid",
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
@@ -276,13 +266,10 @@ func TestLoader_Load(t *testing.T) {
Backend: true,
State: plugins.AlphaRelease,
},
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: plugins.NewLocalFS(
filesInDir(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
),
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
Signature: "unsigned",
},
},
@@ -330,13 +317,10 @@ func TestLoader_Load(t *testing.T) {
Backend: true,
State: plugins.AlphaRelease,
},
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: plugins.NewLocalFS(
filesInDir(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
),
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
Signature: plugins.SignatureUnsigned,
},
},
@@ -440,14 +424,11 @@ func TestLoader_Load(t *testing.T) {
Backend: false,
},
DefaultNavURL: "/plugins/test-app/page/root-page-react",
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(parentDir, "testdata/test-app-with-includes", "dashboards/memory.json"): {},
filepath.Join(parentDir, "testdata/test-app-with-includes", "plugin.json"): {},
}, filepath.Join(parentDir, "testdata/test-app-with-includes")),
Class: plugins.External,
Signature: plugins.SignatureUnsigned,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/test-app-with-includes")),
Class: plugins.External,
Signature: plugins.SignatureUnsigned,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
},
},
},
@@ -532,9 +513,7 @@ func TestLoader_Load_CustomSource(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(parentDir, "testdata/cdn/plugin", "plugin.json"): {},
}, filepath.Join(parentDir, "testdata/cdn/plugin")),
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/cdn/plugin")),
Class: plugins.Bundled,
Signature: plugins.SignatureValid,
BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel",
@@ -670,13 +649,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
Executable: "test",
State: plugins.AlphaRelease,
},
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin/plugin.json"): {},
filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin/MANIFEST.txt"): {},
}, filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin")),
Class: plugins.External,
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin")),
Signature: "valid",
SignatureType: plugins.PrivateSignature,
SignatureOrg: "Will Browne",
@@ -795,10 +771,7 @@ func TestLoader_Load_RBACReady(t *testing.T) {
},
Backend: false,
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(pluginDir, "plugin.json"): {},
filepath.Join(pluginDir, "MANIFEST.txt"): {},
}, pluginDir),
FS: mustNewStaticFSForTests(t, pluginDir),
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.PrivateSignature,
@@ -885,10 +858,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
Backend: true,
Executable: "test",
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"), "plugin.json"): {},
filepath.Join(filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"), "MANIFEST.txt"): {},
}, filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin")),
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin")),
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.PrivateSignature,
@@ -972,7 +942,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
},
Backend: false,
},
FS: plugins.NewLocalFS(filesInDir(t, pluginDir), pluginDir),
FS: mustNewStaticFSForTests(t, pluginDir),
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
@@ -1062,7 +1032,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
},
Backend: false,
},
FS: plugins.NewLocalFS(filesInDir(t, pluginDir1), pluginDir1),
FS: mustNewStaticFSForTests(t, pluginDir1),
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
@@ -1137,10 +1107,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
},
Backend: true,
},
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/nested-plugins/parent")),
filepath.Join(rootDir, "testdata/nested-plugins/parent")),
Module: "plugins/test-datasource/module",
BaseURL: "public/plugins/test-datasource",
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent")),
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
@@ -1170,10 +1139,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
Module: "plugins/test-panel/module",
BaseURL: "public/plugins/test-panel",
FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")),
filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")),
Module: "plugins/test-panel/module",
BaseURL: "public/plugins/test-panel",
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")),
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
@@ -1310,10 +1278,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
},
Backend: false,
},
Module: "plugins/myorgid-simple-app/module",
BaseURL: "public/plugins/myorgid-simple-app",
FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/app-with-child/dist")),
filepath.Join(rootDir, "testdata/app-with-child/dist")),
Module: "plugins/myorgid-simple-app/module",
BaseURL: "public/plugins/myorgid-simple-app",
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/app-with-child/dist")),
DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react",
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
@@ -1349,10 +1316,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
Module: "plugins/myorgid-simple-app/child/module",
BaseURL: "public/plugins/myorgid-simple-app",
FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/app-with-child/dist/child")),
filepath.Join(rootDir, "testdata/app-with-child/dist/child")),
Module: "plugins/myorgid-simple-app/child/module",
BaseURL: "public/plugins/myorgid-simple-app",
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/app-with-child/dist/child")),
IncludedInAppID: parent.ID,
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
@@ -1421,7 +1387,7 @@ func Test_setPathsBasedOnApp(t *testing.T) {
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
signature.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
for _, cb := range cbs {
@@ -1453,39 +1419,8 @@ func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegist
}
}
func filesInDir(t *testing.T, dir string) map[string]struct{} {
files, err := collectFilesWithin(dir)
if err != nil {
t.Logf("Could not collect plugin file info. Err: %v", err)
return map[string]struct{}{}
}
return files
}
func collectFilesWithin(dir string) (map[string]struct{}, error) {
files := map[string]struct{}{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// skip directories
if info.IsDir() {
return nil
}
// verify that file is within plugin directory
//file, err := filepath.Rel(dir, path)
//if err != nil {
// return err
//}
//if strings.HasPrefix(file, ".."+string(filepath.Separator)) {
// return fmt.Errorf("file '%s' not inside of plugin directory", file)
//}
files[path] = struct{}{}
return nil
})
return files, err
func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
require.NoError(t, err)
return sfs
}
@@ -118,7 +118,7 @@ func TestIntegrationPluginManager(t *testing.T) {
reg := registry.ProvideService()
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(), fakes.NewFakeRoleRegistry(),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)
+12 -3
View File
@@ -17,6 +17,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
@@ -29,6 +30,9 @@ var (
// toSlash is filepath.ToSlash, but can be overwritten in tests path separators cross-platform
toSlash = filepath.ToSlash
// fromSlash is filepath.FromSlash, but can be overwritten in tests path separators cross-platform
fromSlash = filepath.FromSlash
)
// PluginManifest holds details for the file manifest
@@ -100,8 +104,11 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
return defaultSignature, nil
}
if len(plugin.FS.Files()) == 0 {
fsFiles, err := plugin.FS.Files()
if err != nil {
return plugins.Signature{}, fmt.Errorf("files: %w", err)
}
if len(fsFiles) == 0 {
s.mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID)
return plugins.Signature{
Status: plugins.SignatureInvalid,
@@ -190,7 +197,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
// Track files missing from the manifest
var unsignedFiles []string
for _, f := range plugin.FS.Files() {
for _, f := range fsFiles {
// Ensure slashes are used, because MANIFEST.txt always uses slashes regardless of the filesystem
f = toSlash(f)
@@ -223,6 +230,8 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
}
func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error {
path = fromSlash(path)
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `path` is based
// on the path provided in a manifest file for a plugin and not user input.
+96 -54
View File
@@ -2,6 +2,7 @@ package signature
import (
"context"
"io/fs"
"path/filepath"
"sort"
"strings"
@@ -172,10 +173,7 @@ func TestCalculate(t *testing.T) {
Version: "1.0.0",
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(basePath, "MANIFEST.txt"): {},
filepath.Join(basePath, "plugin.json"): {},
}, basePath),
FS: mustNewStaticFSForTests(t, basePath),
})
require.NoError(t, err)
require.Equal(t, tc.expectedSignature, sig)
@@ -204,11 +202,7 @@ func TestCalculate(t *testing.T) {
Version: "1.0.0",
},
},
FS: plugins.NewLocalFS(map[string]struct{}{
filepath.Join(basePath, "MANIFEST.txt"): {},
filepath.Join(basePath, "plugin.json"): {},
filepath.Join(basePath, "chrome-win/debug.log"): {},
}, basePath),
FS: mustNewStaticFSForTests(t, basePath),
})
require.NoError(t, err)
require.Equal(t, plugins.Signature{
@@ -219,28 +213,51 @@ func TestCalculate(t *testing.T) {
})
t.Run("Signature verification should work with any path separator", func(t *testing.T) {
var toSlashUnix = newToSlash('/')
var toSlashWindows = newToSlash('\\')
const basePath = "../testdata/app-with-child/dist"
for _, tc := range []struct {
name string
sep string
toSlash func(string) string
platformWindows := fsPlatform{separator: '\\'}
platformUnix := fsPlatform{separator: '/'}
type testCase struct {
name string
platform fsPlatform
fsFactory func() (plugins.FS, error)
}
var testCases []testCase
for _, fsFactory := range []struct {
name string
f func() (plugins.FS, error)
}{
{"unix", "/", toSlashUnix},
{"windows", "\\", toSlashWindows},
{"local fs", func() (plugins.FS, error) {
return plugins.NewLocalFS(basePath), nil
}},
{"static fs", func() (plugins.FS, error) {
return plugins.NewStaticFS(plugins.NewLocalFS(basePath))
}},
} {
testCases = append(testCases, []testCase{
{"unix " + fsFactory.name, platformUnix, fsFactory.f},
{"windows " + fsFactory.name, platformWindows, fsFactory.f},
}...)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Replace toSlash for cross-platform testing
oldToSlash := toSlash
oldFromSlash := fromSlash
t.Cleanup(func() {
toSlash = oldToSlash
fromSlash = oldFromSlash
})
toSlash = tc.toSlash
basePath := "../testdata/app-with-child/dist"
toSlash = tc.platform.toSlashFunc()
fromSlash = tc.platform.fromSlashFunc()
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
pfs, err := tc.fsFactory()
require.NoError(t, err)
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
require.NoError(t, err)
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
@@ -253,11 +270,7 @@ func TestCalculate(t *testing.T) {
Version: "%VERSION%",
},
},
FS: newPathSeparatorOverrideFS(tc.sep, map[string]struct{}{
filepath.Join(basePath, "MANIFEST.txt"): {},
filepath.Join(basePath, "plugin.json"): {},
filepath.Join(basePath, "child/plugin.json"): {},
}, basePath),
FS: pfs,
})
require.NoError(t, err)
require.Equal(t, plugins.Signature{
@@ -270,20 +283,35 @@ func TestCalculate(t *testing.T) {
})
}
// newToSlash returns a new function that acts as filepath.ToSlash but for the specified os-separator.
type fsPlatform struct {
separator rune
}
// toSlashFunc returns a new function that acts as filepath.ToSlash but for the specified os-separator.
// This can be used to test filepath.ToSlash-dependant code cross-platform.
func newToSlash(sep rune) func(string) string {
func (p fsPlatform) toSlashFunc() func(string) string {
return func(path string) string {
if sep == '/' {
if p.separator == '/' {
return path
}
return strings.ReplaceAll(path, string(sep), "/")
return strings.ReplaceAll(path, string(p.separator), "/")
}
}
func TestNewToSlash(t *testing.T) {
// fromSlashFunc returns a new function that acts as filepath.FromSlash but for the specified os-separator.
// This can be used to test filepath.FromSlash-dependant code cross-platform.
func (p fsPlatform) fromSlashFunc() func(string) string {
return func(path string) string {
if p.separator == '/' {
return path
}
return strings.ReplaceAll(path, "/", string(p.separator))
}
}
func TestFsPlatform(t *testing.T) {
t.Run("unix", func(t *testing.T) {
toSlashUnix := newToSlash('/')
toSlashUnix := fsPlatform{'/'}.toSlashFunc()
require.Equal(t, "folder", toSlashUnix("folder"))
require.Equal(t, "/folder", toSlashUnix("/folder"))
require.Equal(t, "/folder/file", toSlashUnix("/folder/file"))
@@ -291,39 +319,46 @@ func TestNewToSlash(t *testing.T) {
})
t.Run("windows", func(t *testing.T) {
toSlashWindows := newToSlash('\\')
toSlashWindows := fsPlatform{'\\'}.toSlashFunc()
require.Equal(t, "folder", toSlashWindows("folder"))
require.Equal(t, "C:/folder", toSlashWindows("C:\\folder"))
require.Equal(t, "folder/file.exe", toSlashWindows("folder\\file.exe"))
})
}
// fsPathSeparatorFiles embeds plugins.LocalFS and overrides the Files() behaviour so all the returned elements
// fsPathSeparatorFiles embeds a plugins.FS and overrides the Files() behaviour so all the returned elements
// have the specified path separator. This can be used to test Files() behaviour cross-platform.
type fsPathSeparatorFiles struct {
plugins.LocalFS
plugins.FS
separator string
}
// newPathSeparatorOverrideFS returns a new fsPathSeparatorFiles. Sep is the separator that will be used ONLY for
// the elements returned by Files(). Files and basePath MUST use the os-specific path separator (filepath.Separator)
// if Open() is required to work for the test case.
func newPathSeparatorOverrideFS(sep string, files map[string]struct{}, basePath string) fsPathSeparatorFiles {
// the elements returned by Files().
func newPathSeparatorOverrideFS(sep string, ufs plugins.FS) (fsPathSeparatorFiles, error) {
return fsPathSeparatorFiles{
LocalFS: plugins.NewLocalFS(files, basePath),
FS: ufs,
separator: sep,
}
}, nil
}
// Files returns LocalFS.Files(), but all path separators (filepath.Separator) are replaced with f.separator.
func (f fsPathSeparatorFiles) Files() []string {
files := f.LocalFS.Files()
// Files returns LocalFS.Files(), but all path separators for the current platform (filepath.Separator)
// are replaced with f.separator.
func (f fsPathSeparatorFiles) Files() ([]string, error) {
files, err := f.FS.Files()
if err != nil {
return nil, err
}
const osSepStr = string(filepath.Separator)
for i := 0; i < len(files); i++ {
files[i] = strings.ReplaceAll(files[i], osSepStr, f.separator)
}
return files
return files, nil
}
func (f fsPathSeparatorFiles) Open(name string) (fs.File, error) {
return f.FS.Open(strings.ReplaceAll(name, f.separator, string(filepath.Separator)))
}
func TestFSPathSeparatorFiles(t *testing.T) {
@@ -335,17 +370,18 @@ func TestFSPathSeparatorFiles(t *testing.T) {
{"windows", "\\"},
} {
t.Run(tc.name, func(t *testing.T) {
fs := newPathSeparatorOverrideFS("/", map[string]struct{}{
"a": {},
strings.Join([]string{"a", "b", "c"}, tc.sep): {},
}, ".")
files := fs.Files()
filesMap := make(map[string]struct{}, len(files))
// Re-convert to map as the key order is not stable
for _, f := range files {
filesMap[f] = struct{}{}
}
require.Equal(t, filesMap, map[string]struct{}{"a": {}, strings.Join([]string{"a", "b", "c"}, tc.sep): {}})
pfs, err := newPathSeparatorOverrideFS(
"/", plugins.NewInMemoryFS(
map[string][]byte{"a": nil, strings.Join([]string{"a", "b", "c"}, tc.sep): nil},
),
)
require.NoError(t, err)
files, err := pfs.Files()
require.NoError(t, err)
exp := []string{"a", strings.Join([]string{"a", "b", "c"}, tc.sep)}
sort.Strings(files)
sort.Strings(exp)
require.Equal(t, exp, files)
})
}
}
@@ -715,3 +751,9 @@ func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifes
return m
}
func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
require.NoError(t, err)
return sfs
}