Plugins: Simplify plugin file removal (#66115)

* make explicit class check when attempting to remove plugin

* simplify plugin file tracking

* fix test

* apply feedback

* fix linter
This commit is contained in:
Will Browne
2023-04-20 10:52:59 +01:00
committed by GitHub
parent 6e8b17efd8
commit 739c7f1c68
15 changed files with 193 additions and 281 deletions

View File

@ -12,36 +12,27 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/grafana/grafana/pkg/plugins/log"
)
var _ Manager = (*FS)(nil)
var _ ZipExtractor = (*FS)(nil)
var reGitBuild = regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
var (
ErrUninstallOutsideOfPluginDir = errors.New("cannot uninstall a plugin outside of the plugins directory")
ErrUninstallInvalidPluginDir = errors.New("cannot recognize as plugin folder")
)
type FS struct {
store map[string]string
mu sync.RWMutex
pluginsDir string
log log.PrettyLogger
}
func FileSystem(logger log.PrettyLogger, pluginsDir string) *FS {
return &FS{
store: make(map[string]string),
pluginsDir: pluginsDir,
log: logger,
}
}
func (fs *FS) Add(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) (
func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) (
*ExtractedPluginArchive, error) {
pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID)
if err != nil {
@ -71,38 +62,6 @@ func (fs *FS) Add(ctx context.Context, pluginID string, pluginArchive *zip.ReadC
}, nil
}
func (fs *FS) Register(_ context.Context, pluginID, pluginDir string) error {
fs.mu.Lock()
fs.store[pluginID] = pluginDir
fs.mu.Unlock()
return nil
}
func (fs *FS) Remove(_ context.Context, pluginID string) error {
fs.mu.RLock()
pluginDir, exists := fs.store[pluginID]
fs.mu.RUnlock()
if !exists {
return fmt.Errorf("%s does not exist", pluginID)
}
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
path, err := filepath.Rel(fs.pluginsDir, pluginDir)
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
return ErrUninstallOutsideOfPluginDir
}
if _, err = os.Stat(filepath.Join(pluginDir, "plugin.json")); os.IsNotExist(err) {
if _, err = os.Stat(filepath.Join(pluginDir, "dist/plugin.json")); os.IsNotExist(err) {
return ErrUninstallInvalidPluginDir
}
}
fs.log.Infof("Uninstalling plugin %v", pluginDir)
return os.RemoveAll(pluginDir)
}
func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string) (string, error) {
installDir := filepath.Join(fs.pluginsDir, pluginID)
if _, err := os.Stat(installDir); !os.IsNotExist(err) {
@ -261,7 +220,7 @@ func removeGitBuildFromName(filename, pluginID string) string {
return reGitBuild.ReplaceAllString(filename, pluginID+"/")
}
func toPluginDTO(pluginID, pluginDir string) (InstalledPlugin, error) {
func toPluginDTO(pluginID, pluginDir string) (installedPlugin, error) {
distPluginDataPath := filepath.Join(pluginDir, "dist", "plugin.json")
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
@ -273,17 +232,17 @@ func toPluginDTO(pluginID, pluginDir string) (InstalledPlugin, error) {
// nolint:gosec
data, err = os.ReadFile(pluginDataPath)
if err != nil {
return InstalledPlugin{}, fmt.Errorf("could not find dist/plugin.json or plugin.json for %s in %s", pluginID, pluginDir)
return installedPlugin{}, fmt.Errorf("could not find dist/plugin.json or plugin.json for %s in %s", pluginID, pluginDir)
}
}
res := InstalledPlugin{}
res := installedPlugin{}
if err = json.Unmarshal(data, &res); err != nil {
return res, err
}
if res.ID == "" {
return InstalledPlugin{}, fmt.Errorf("could not find valid plugin %s in %s", pluginID, pluginDir)
return installedPlugin{}, fmt.Errorf("could not find valid plugin %s in %s", pluginID, pluginDir)
}
if res.Info.Version == "" {

View File

@ -25,7 +25,7 @@ func TestAdd(t *testing.T) {
pluginID := "test-app"
fs := FileSystem(&fakeLogger{}, testDir)
archive, err := fs.Add(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
require.NotNil(t, archive)
require.NoError(t, err)
@ -48,89 +48,6 @@ func TestAdd(t *testing.T) {
require.Equal(t, files[5].Name(), "text.txt")
}
func TestRemove(t *testing.T) {
pluginDir := t.TempDir()
pluginJSON := filepath.Join(pluginDir, "plugin.json")
//nolint:gosec
_, err := os.Create(pluginJSON)
require.NoError(t, err)
pluginID := "test-datasource"
i := &FS{
pluginsDir: filepath.Dir(pluginDir),
store: map[string]string{
pluginID: pluginDir,
},
log: &fakeLogger{},
}
err = i.Remove(context.Background(), pluginID)
require.NoError(t, err)
_, err = os.Stat(pluginDir)
require.True(t, os.IsNotExist(err))
t.Run("Uninstall will search in nested dir folder for plugin.json", func(t *testing.T) {
pluginDistDir := filepath.Join(t.TempDir(), "dist")
err = os.Mkdir(pluginDistDir, os.ModePerm)
require.NoError(t, err)
pluginJSON = filepath.Join(pluginDistDir, "plugin.json")
//nolint:gosec
_, err = os.Create(pluginJSON)
require.NoError(t, err)
pluginDir = filepath.Dir(pluginDistDir)
i = &FS{
pluginsDir: filepath.Dir(pluginDir),
store: map[string]string{
pluginID: pluginDir,
},
log: &fakeLogger{},
}
err = i.Remove(context.Background(), pluginID)
require.NoError(t, err)
_, err = os.Stat(pluginDir)
require.True(t, os.IsNotExist(err))
})
t.Run("Uninstall will not delete folder if cannot recognize plugin structure", func(t *testing.T) {
pluginDir = t.TempDir()
i = &FS{
pluginsDir: filepath.Dir(pluginDir),
store: map[string]string{
pluginID: pluginDir,
},
log: &fakeLogger{},
}
err = i.Remove(context.Background(), pluginID)
require.EqualError(t, err, "cannot recognize as plugin folder")
_, err = os.Stat(pluginDir)
require.False(t, os.IsNotExist(err))
})
t.Run("Uninstall will not delete folder if plugin's directory is not a subdirectory of specified plugins directory", func(t *testing.T) {
pluginDir = t.TempDir()
i = &FS{
pluginsDir: "/some/other/path",
store: map[string]string{
pluginID: pluginDir,
},
log: &fakeLogger{},
}
err = i.Remove(context.Background(), pluginID)
require.EqualError(t, err, "cannot uninstall a plugin outside of the plugins directory")
_, err = os.Stat(pluginDir)
require.False(t, os.IsNotExist(err))
})
}
func TestExtractFiles(t *testing.T) {
pluginsDir := setupFakePluginsDir(t)

View File

@ -5,8 +5,6 @@ import (
"context"
)
type Manager interface {
Add(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
Register(ctx context.Context, pluginID, pluginDir string) error
Remove(ctx context.Context, pluginID string) error
type ZipExtractor interface {
Extract(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
}

View File

@ -22,27 +22,27 @@ type Dependency struct {
Version string
}
type InstalledPlugin struct {
type installedPlugin struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Info PluginInfo `json:"info"`
Dependencies Dependencies `json:"dependencies"`
Info pluginInfo `json:"info"`
Dependencies dependencies `json:"dependencies"`
}
type Dependencies struct {
type dependencies struct {
GrafanaVersion string `json:"grafanaVersion"`
Plugins []PluginDependency `json:"plugins"`
Plugins []pluginDependency `json:"plugins"`
}
type PluginDependency struct {
type pluginDependency struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Version string `json:"version"`
}
type PluginInfo struct {
type pluginInfo struct {
Version string `json:"version"`
Updated string `json:"updated"`
}