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