mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 21:32:22 +08:00
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:
@ -66,7 +66,7 @@ func TestCallResource(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
reg := registry.ProvideService()
|
||||
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), 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)
|
||||
|
@ -277,7 +277,7 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
requestedFile := filepath.Clean(tmpFile.Name())
|
||||
|
||||
t.Run("Given a request for an existing plugin file", func(t *testing.T) {
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{requestedFile: {}}, filepath.Dir(requestedFile)))
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(filepath.Dir(requestedFile)))
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
@ -295,7 +295,7 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for a relative path", func(t *testing.T) {
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewFakeFS())
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
@ -312,9 +312,7 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{
|
||||
requestedFile: {},
|
||||
}, ""))
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.Core, plugins.NewLocalFS(filepath.Dir(requestedFile)))
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
@ -332,7 +330,7 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewFakeFS())
|
||||
service := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
@ -625,13 +623,13 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
||||
ID: "test-app", Type: "app", Name: "test-app",
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
}}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
}}, plugins.External, plugins.NewFakeFS())
|
||||
p2 := createPlugin(
|
||||
plugins.JSONData{ID: "mysql", Type: "datasource", Name: "MySQL",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"},
|
||||
Description: "Data source for MySQL databases",
|
||||
}}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
}}, plugins.Core, plugins.NewFakeFS())
|
||||
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
|
@ -55,7 +55,7 @@ type FS interface {
|
||||
fs.FS
|
||||
|
||||
Base() string
|
||||
Files() []string
|
||||
Files() ([]string, error)
|
||||
}
|
||||
|
||||
type FSRemover interface {
|
||||
|
@ -2,6 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -10,78 +11,152 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var _ fs.FS = (*LocalFS)(nil)
|
||||
var (
|
||||
_ fs.File = &LocalFile{}
|
||||
|
||||
_ FS = &LocalFS{}
|
||||
_ FS = &StaticFS{}
|
||||
)
|
||||
|
||||
// LocalFS is a plugins.FS that allows accessing files on the local file system.
|
||||
type LocalFS struct {
|
||||
// m is a map of relative file paths that can be accessed on the local filesystem.
|
||||
// The path separator must be os-specific.
|
||||
m map[string]*LocalFile
|
||||
|
||||
// basePath is the basePath that will be prepended to all the files (in m map) before accessing them.
|
||||
// basePath is the basePath that will be prepended to all the files to get their absolute path.
|
||||
basePath string
|
||||
}
|
||||
|
||||
// NewLocalFS returns a new LocalFS that can access the specified files in the specified base path.
|
||||
// Both the map keys and basePath should use the os-specific path separator for Open() to work properly.
|
||||
func NewLocalFS(m map[string]struct{}, basePath string) LocalFS {
|
||||
pfs := make(map[string]*LocalFile, len(m))
|
||||
for k := range m {
|
||||
pfs[k] = &LocalFile{
|
||||
path: k,
|
||||
// NewLocalFS returns a new LocalFS that can access any file in the specified base path on the filesystem.
|
||||
// basePath must use os-specific path separator for Open() to work properly.
|
||||
func NewLocalFS(basePath string) LocalFS {
|
||||
return LocalFS{basePath: basePath}
|
||||
}
|
||||
|
||||
// fileIsAllowed takes an absolute path to a file and an os.FileInfo for that file, and it checks if access to that
|
||||
// file is allowed or not. Access to a file is allowed if the file is in the FS's Base() directory, and if it's a
|
||||
// symbolic link it should not end up outside the plugin's directory.
|
||||
func (f LocalFS) fileIsAllowed(basePath string, absolutePath string, info os.FileInfo) (bool, error) {
|
||||
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
symlinkPath, err := filepath.EvalSymlinks(absolutePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
symlink, err := os.Stat(symlinkPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// verify that symlinked file is within plugin directory
|
||||
p, err := filepath.Rel(basePath, symlinkPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) {
|
||||
return false, fmt.Errorf("file '%s' not inside of plugin directory", p)
|
||||
}
|
||||
|
||||
// skip adding symlinked directories
|
||||
if symlink.IsDir() {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return LocalFS{
|
||||
m: pfs,
|
||||
basePath: basePath,
|
||||
// skip directories
|
||||
if info.IsDir() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// verify that file is within plugin directory
|
||||
file, err := filepath.Rel(f.Base(), absolutePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.HasPrefix(file, ".."+string(filepath.Separator)) {
|
||||
return false, fmt.Errorf("file '%s' not inside of plugin directory", file)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// walkFunc returns a filepath.WalkFunc that accumulates absolute file paths into acc by walking over f.Base().
|
||||
// f.fileIsAllowed is used as WalkFunc, see its documentation for more information on which files are collected.
|
||||
func (f LocalFS) walkFunc(basePath string, acc map[string]struct{}) filepath.WalkFunc {
|
||||
return func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := f.fileIsAllowed(basePath, path, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
acc[path] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens the specified file on the local filesystem, and returns the corresponding fs.File.
|
||||
// If a nil error is returned, the caller should take care of closing the returned file.
|
||||
// Open opens the specified file on the local filesystem.
|
||||
// The provided name must be a relative file name that uses os-specific path separators.
|
||||
// The function returns the corresponding fs.File for accessing the file on the local filesystem.
|
||||
// If a nil error is returned, the caller should take care of calling Close() the returned fs.File.
|
||||
// If the file does not exist, ErrFileNotExist is returned.
|
||||
func (f LocalFS) Open(name string) (fs.File, error) {
|
||||
cleanPath, err := util.CleanRelativePath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if kv, exists := f.m[filepath.Join(f.basePath, cleanPath)]; exists {
|
||||
if kv.f != nil {
|
||||
return kv.f, nil
|
||||
}
|
||||
file, err := os.Open(kv.path)
|
||||
basePath := f.Base()
|
||||
absFn := filepath.Join(basePath, cleanPath)
|
||||
finfo, err := os.Stat(absFn)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, ErrFileNotExist
|
||||
}
|
||||
return nil, ErrPluginFileRead
|
||||
}
|
||||
return file, nil
|
||||
// Make sure access to the file is allowed (symlink check, etc)
|
||||
ok, err := f.fileIsAllowed(basePath, absFn, finfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrFileNotExist
|
||||
}
|
||||
return &LocalFile{path: absFn}, nil
|
||||
}
|
||||
|
||||
// Base returns the base path for the LocalFS.
|
||||
// The returned string uses os-specific path separator.
|
||||
func (f LocalFS) Base() string {
|
||||
return f.basePath
|
||||
}
|
||||
|
||||
// Files returns a slice of all the file paths in the LocalFS relative to the base path.
|
||||
// The returned strings use the same path separator as the
|
||||
func (f LocalFS) Files() []string {
|
||||
var files []string
|
||||
for p := range f.m {
|
||||
r, err := filepath.Rel(f.basePath, p)
|
||||
if strings.Contains(r, "..") || err != nil {
|
||||
// Files returns a slice of all the relative file paths on the LocalFS.
|
||||
// The returned strings can be passed to Open() to open those files.
|
||||
// The returned strings use os-specific path separator.
|
||||
func (f LocalFS) Files() ([]string, error) {
|
||||
// Accumulate all files into filesMap by calling f.collectFilesFunc, which will write into the accumulator.
|
||||
// Those are absolute because filepath.Walk uses absolute file paths.
|
||||
absFilePaths := make(map[string]struct{})
|
||||
if err := filepath.Walk(f.basePath, f.walkFunc(f.Base(), absFilePaths)); err != nil {
|
||||
return nil, fmt.Errorf("walk: %w", err)
|
||||
}
|
||||
// Convert the accumulator into a slice of relative path strings
|
||||
relFiles := make([]string, 0, len(absFilePaths))
|
||||
base := f.Base()
|
||||
for fn := range absFilePaths {
|
||||
relPath, err := filepath.Rel(base, fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clenRelPath, err := util.CleanRelativePath(relPath)
|
||||
if strings.Contains(clenRelPath, "..") || err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, r)
|
||||
relFiles = append(relFiles, clenRelPath)
|
||||
}
|
||||
|
||||
return files
|
||||
return relFiles, nil
|
||||
}
|
||||
|
||||
// Remove removes a plugin from the local filesystem by deleting all files in the folder.
|
||||
// It returns ErrUninstallInvalidPluginDir is the plugin does not contain plugin.json nor dist/plugin.json.
|
||||
func (f LocalFS) Remove() error {
|
||||
// extra security check to ensure we only remove a directory that looks like a plugin
|
||||
if _, err := os.Stat(filepath.Join(f.basePath, "plugin.json")); os.IsNotExist(err) {
|
||||
@ -89,11 +164,72 @@ func (f LocalFS) Remove() error {
|
||||
return ErrUninstallInvalidPluginDir
|
||||
}
|
||||
}
|
||||
|
||||
return os.RemoveAll(f.basePath)
|
||||
}
|
||||
|
||||
var _ fs.File = (*LocalFile)(nil)
|
||||
// staticFilesMap is a set-like map that contains files that can be accessed from a plugins.FS.
|
||||
type staticFilesMap map[string]struct{}
|
||||
|
||||
// isAllowed returns true if the provided path is allowed.
|
||||
// path is a string accepted by an FS Open() method.
|
||||
func (a staticFilesMap) isAllowed(path string) bool {
|
||||
_, ok := a[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
// newStaticFilesMap creates a new staticFilesMap from a list of allowed file paths.
|
||||
func newStaticFilesMap(files ...string) staticFilesMap {
|
||||
m := staticFilesMap(make(map[string]struct{}, len(files)))
|
||||
for _, k := range files {
|
||||
m[k] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// StaticFS wraps an FS and allows accessing only the files in the allowList.
|
||||
// This is a more secure implementation of a FS suitable for production environments.
|
||||
// The keys of the allow list must be in the same format used by the underlying FS' Open() method.
|
||||
type StaticFS struct {
|
||||
FS
|
||||
|
||||
// staticFilesMap is a map of allowed paths (accepted by FS.Open())
|
||||
staticFilesMap staticFilesMap
|
||||
}
|
||||
|
||||
// NewStaticFS returns a new StaticFS that can access the files on an underlying FS,
|
||||
// but only if they are also specified in a static list, which is constructed when creating the object
|
||||
// by calling Files() on the underlying FS.
|
||||
func NewStaticFS(fs FS) (StaticFS, error) {
|
||||
files, err := fs.Files()
|
||||
if err != nil {
|
||||
return StaticFS{}, err
|
||||
}
|
||||
return StaticFS{
|
||||
FS: fs,
|
||||
staticFilesMap: newStaticFilesMap(files...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Open checks that name is an allowed file and, if so, it returns a fs.File to access it, by calling the
|
||||
// underlying FS' Open() method.
|
||||
// If access is denied, the function returns ErrFileNotExist.
|
||||
func (f StaticFS) Open(name string) (fs.File, error) {
|
||||
// Ensure access to the file is allowed
|
||||
if !f.staticFilesMap.isAllowed(name) {
|
||||
return nil, ErrFileNotExist
|
||||
}
|
||||
// Use the wrapped FS to access the file
|
||||
return f.FS.Open(name)
|
||||
}
|
||||
|
||||
// Files returns a slice of all static file paths relative to the base path.
|
||||
func (f StaticFS) Files() ([]string, error) {
|
||||
files := make([]string, 0, len(f.staticFilesMap))
|
||||
for fn := range f.staticFilesMap {
|
||||
files = append(files, fn)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// LocalFile implements a fs.File for accessing the local filesystem.
|
||||
type LocalFile struct {
|
||||
|
@ -1,9 +1,11 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -17,14 +19,7 @@ func TestLocalFS_Remove(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
fs := NewLocalFS(
|
||||
map[string]struct{}{
|
||||
"plugin.json": {},
|
||||
},
|
||||
pluginDir,
|
||||
)
|
||||
|
||||
fs := NewLocalFS(pluginDir)
|
||||
err = fs.Remove()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -49,12 +44,7 @@ func TestLocalFS_Remove(t *testing.T) {
|
||||
|
||||
pluginDir = filepath.Dir(pluginDistDir)
|
||||
|
||||
fs = NewLocalFS(
|
||||
map[string]struct{}{
|
||||
"dist/plugin.json": {},
|
||||
},
|
||||
pluginDir,
|
||||
)
|
||||
fs = NewLocalFS(pluginDir)
|
||||
|
||||
err = fs.Remove()
|
||||
require.NoError(t, err)
|
||||
@ -78,12 +68,7 @@ func TestLocalFS_Remove(t *testing.T) {
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
fs = NewLocalFS(
|
||||
map[string]struct{}{
|
||||
"system32/important.exe": {},
|
||||
},
|
||||
pluginDir,
|
||||
)
|
||||
fs = NewLocalFS(pluginDir)
|
||||
|
||||
err = fs.Remove()
|
||||
require.ErrorIs(t, err, ErrUninstallInvalidPluginDir)
|
||||
@ -222,3 +207,73 @@ func newTempFileScenarioForTest(t *testing.T) tempFileScenario {
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
func createDummyTempFile(dir, fn string) (err error) {
|
||||
f, err := os.Create(filepath.Join(dir, fn)) // nolint: gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := f.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
_, err = f.WriteString(fn)
|
||||
return
|
||||
}
|
||||
|
||||
func TestStaticFS(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
const allowedFn, deniedFn = "allowed.txt", "denied.txt"
|
||||
require.NoError(t, createDummyTempFile(tmp, allowedFn))
|
||||
|
||||
localFS := NewLocalFS(tmp)
|
||||
staticFS, err := NewStaticFS(localFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("open allowed", func(t *testing.T) {
|
||||
f, err := staticFS.Open(allowedFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
b, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(allowedFn), b)
|
||||
})
|
||||
|
||||
t.Run("open denied", func(t *testing.T) {
|
||||
// Add file after initialization
|
||||
require.NoError(t, createDummyTempFile(tmp, deniedFn))
|
||||
|
||||
// StaticFS should fail
|
||||
_, err := staticFS.Open(deniedFn)
|
||||
require.True(t, errors.Is(err, ErrFileNotExist))
|
||||
|
||||
// Underlying FS should succeed
|
||||
f, err := localFS.Open(deniedFn)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
b, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("denied.txt"), b)
|
||||
})
|
||||
|
||||
t.Run("open not existing", func(t *testing.T) {
|
||||
_, err := staticFS.Open("unknown.txt")
|
||||
require.True(t, errors.Is(err, ErrFileNotExist))
|
||||
})
|
||||
|
||||
t.Run("list files", func(t *testing.T) {
|
||||
t.Run("underlying fs has extra files", func(t *testing.T) {
|
||||
files, err := localFS.Files()
|
||||
require.NoError(t, err)
|
||||
sort.Strings(files)
|
||||
require.Equal(t, []string{allowedFn, deniedFn}, files)
|
||||
})
|
||||
|
||||
t.Run("staticfs filters underelying fs's files", func(t *testing.T) {
|
||||
files, err := staticFS.Files()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{allowedFn}, files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
)
|
||||
@ -25,14 +26,20 @@ var (
|
||||
|
||||
type Local struct {
|
||||
log log.Logger
|
||||
production bool
|
||||
}
|
||||
|
||||
func NewLocalFinder() *Local {
|
||||
func NewLocalFinder(cfg *config.Cfg) *Local {
|
||||
return &Local{
|
||||
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)
|
||||
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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
@ -149,10 +152,7 @@ func TestLoader_Load(t *testing.T) {
|
||||
},
|
||||
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/"),
|
||||
),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/")),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@ -229,17 +229,7 @@ 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"),
|
||||
),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@ -279,10 +269,7 @@ func TestLoader_Load(t *testing.T) {
|
||||
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"),
|
||||
),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
|
||||
Signature: "unsigned",
|
||||
},
|
||||
},
|
||||
@ -333,10 +320,7 @@ func TestLoader_Load(t *testing.T) {
|
||||
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"),
|
||||
),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")),
|
||||
Signature: plugins.SignatureUnsigned,
|
||||
},
|
||||
},
|
||||
@ -440,10 +424,7 @@ 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")),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/test-app-with-includes")),
|
||||
Class: plugins.External,
|
||||
Signature: plugins.SignatureUnsigned,
|
||||
Module: "plugins/test-app/module",
|
||||
@ -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",
|
||||
@ -673,10 +652,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
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")),
|
||||
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,
|
||||
@ -1139,8 +1109,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
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")),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent")),
|
||||
Signature: plugins.SignatureValid,
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@ -1172,8 +1141,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
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")),
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")),
|
||||
Signature: plugins.SignatureValid,
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@ -1312,8 +1280,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
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")),
|
||||
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,
|
||||
@ -1351,8 +1318,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
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")),
|
||||
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)
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
platformWindows := fsPlatform{separator: '\\'}
|
||||
platformUnix := fsPlatform{separator: '/'}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
sep string
|
||||
toSlash func(string) 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
|
||||
}
|
||||
|
82
pkg/plugins/test_utils.go
Normal file
82
pkg/plugins/test_utils.go
Normal file
@ -0,0 +1,82 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
_ FS = &inMemoryFS{}
|
||||
_ fs.File = &inMemoryFile{}
|
||||
_ fs.FileInfo = &inMemoryFileInfo{}
|
||||
)
|
||||
|
||||
// inMemoryFS is an FS that stores files in-memory.
|
||||
type inMemoryFS struct {
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
// NewInMemoryFS returns a new FS with the specified files and content.
|
||||
// The provided value is a map from file name (keys) to file content (values).
|
||||
func NewInMemoryFS(files map[string][]byte) FS {
|
||||
return &inMemoryFS{files: files}
|
||||
}
|
||||
|
||||
func (f inMemoryFS) Base() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f inMemoryFS) Files() ([]string, error) {
|
||||
fps := make([]string, 0, len(f.files))
|
||||
for fn := range f.files {
|
||||
fps = append(fps, fn)
|
||||
}
|
||||
return fps, nil
|
||||
}
|
||||
|
||||
func (f inMemoryFS) Open(fn string) (fs.File, error) {
|
||||
if _, ok := f.files[fn]; !ok {
|
||||
return nil, ErrFileNotExist
|
||||
}
|
||||
return &inMemoryFile{path: fn, reader: bytes.NewReader(f.files[fn])}, nil
|
||||
}
|
||||
|
||||
// NewFakeFS returns a new FS that always returns ErrFileNotExist when trying to Open() and empty Files().
|
||||
func NewFakeFS() FS {
|
||||
return NewInMemoryFS(nil)
|
||||
}
|
||||
|
||||
// inMemoryFile is a fs.File whose content is stored in memory.
|
||||
type inMemoryFile struct {
|
||||
path string
|
||||
reader *bytes.Reader
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) Stat() (fs.FileInfo, error) {
|
||||
return inMemoryFileInfo{
|
||||
name: f.path,
|
||||
size: f.reader.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) Read(b []byte) (int, error) {
|
||||
return f.reader.Read(b)
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type inMemoryFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
|
||||
func (f inMemoryFileInfo) Name() string { return f.name }
|
||||
func (f inMemoryFileInfo) Size() int64 { return f.size }
|
||||
func (f inMemoryFileInfo) Mode() os.FileMode { return 0444 } // Read for all
|
||||
func (f inMemoryFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (f inMemoryFileInfo) IsDir() bool { return false }
|
||||
func (f inMemoryFileInfo) Sys() interface{} { return nil }
|
@ -81,7 +81,7 @@ var WireExtensionSet = wire.NewSet(
|
||||
signature.ProvideOSSAuthorizer,
|
||||
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
|
||||
wire.Bind(new(finder.Finder), new(*finder.Local)),
|
||||
finder.NewLocalFinder,
|
||||
finder.ProvideLocalFinder,
|
||||
)
|
||||
|
||||
func ProvideClientDecorator(
|
||||
|
Reference in New Issue
Block a user