CLI: Allow relative symlinks in zip archives when installing plugins (#50537)

Earlier we only allowed symlinks in plugins starting with grafana- in zip archives when 
installing plugins using the grafana-cli. This changes so that symlinks in zip archives 
containing relative links to files in the zip archive are always allowed when installing 
plugins. The reasoning behind this is that Grafana per default doesn't load a plugin 
that has an invalid plugin signature meaning that any symlink must be included in 
the plugin signature manifest.

Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
Marcus Efraimsson
2022-06-15 14:38:59 +02:00
committed by GitHub
parent 68691d7775
commit b47ec36d0d
14 changed files with 397 additions and 562 deletions

View File

@ -1,268 +0,0 @@
package commands
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/commandstest"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoveGitBuildFromName(t *testing.T) {
pluginName := "datasource-kairosdb"
// The root directory should get renamed to the plugin name
paths := map[string]string{
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/": "datasource-kairosdb/",
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/README.md": "datasource-kairosdb/README.md",
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/": "datasource-kairosdb/partials/",
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/config.html": "datasource-kairosdb/partials/config.html",
}
for pth, exp := range paths {
name := removeGitBuildFromName(pluginName, pth)
assert.Equal(t, exp, name)
}
}
func TestExtractFiles(t *testing.T) {
t.Run("Should preserve file permissions for plugin backend binaries for linux and darwin", func(t *testing.T) {
skipWindows(t)
pluginsDir := setupFakePluginsDir(t)
archive := filepath.Join("testdata", "grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
err := extractFiles(archive, "grafana-simple-json-datasource", pluginsDir, false)
require.NoError(t, err)
// File in zip has permissions 755
fileInfo, err := os.Stat(filepath.Join(pluginsDir, "grafana-simple-json-datasource",
"simple-plugin_darwin_amd64"))
require.NoError(t, err)
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
// File in zip has permission 755
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_linux_amd64")
require.NoError(t, err)
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
// File in zip has permission 644
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
require.NoError(t, err)
assert.Equal(t, "-rw-r--r--", fileInfo.Mode().String())
// File in zip has permission 755
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary")
require.NoError(t, err)
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
})
t.Run("Should ignore symlinks if not allowed", func(t *testing.T) {
pluginsDir := setupFakePluginsDir(t)
err := extractFiles("testdata/plugin-with-symlink.zip", "plugin-with-symlink", pluginsDir, false)
require.NoError(t, err)
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/text.txt")
require.NoError(t, err)
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
assert.Error(t, err)
})
t.Run("Should extract symlinks if allowed", func(t *testing.T) {
skipWindows(t)
pluginsDir := setupFakePluginsDir(t)
err := extractFiles("testdata/plugin-with-symlink.zip", "plugin-with-symlink", pluginsDir, true)
require.NoError(t, err)
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
require.NoError(t, err)
})
t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) {
pluginsDir := setupFakePluginsDir(t)
err := extractFiles("testdata/plugin-with-parent-member.zip", "plugin-with-parent-member",
pluginsDir, true)
require.EqualError(t, err, fmt.Sprintf(
`archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
pluginsDir,
))
})
t.Run("Should detect if archive members are absolute", func(t *testing.T) {
pluginsDir := setupFakePluginsDir(t)
err := extractFiles("testdata/plugin-with-absolute-member.zip", "plugin-with-absolute-member",
pluginsDir, true)
require.EqualError(t, err, fmt.Sprintf(
`archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
pluginsDir,
))
})
}
func TestInstallPluginCommand(t *testing.T) {
pluginsDir := setupFakePluginsDir(t)
c, err := commandstest.NewCliContext(map[string]string{"pluginsDir": pluginsDir})
require.NoError(t, err)
client := &commandstest.FakeGrafanaComClient{
GetPluginFunc: func(pluginId, repoUrl string) (models.Plugin, error) {
require.Equal(t, "test-plugin-panel", pluginId)
plugin := models.Plugin{
ID: "test-plugin-panel",
Category: "",
Versions: []models.Version{
{
Commit: "commit",
URL: "url",
Version: "1.0.0",
Arch: map[string]models.ArchMeta{
fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH): {
SHA256: "test",
},
},
},
},
}
return plugin, nil
},
DownloadFileFunc: func(pluginName string, tmpFile *os.File, url string, checksum string) (err error) {
require.Equal(t, "test-plugin-panel", pluginName)
require.Equal(t, "/test-plugin-panel/versions/1.0.0/download", url)
require.Equal(t, "test", checksum)
f, err := os.Open("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
require.NoError(t, err)
_, err = io.Copy(tmpFile, f)
require.NoError(t, err)
return nil
},
}
err = InstallPlugin("test-plugin-panel", "", c, client)
assert.NoError(t, err)
}
func TestSelectVersion(t *testing.T) {
t.Run("Should return error when requested version does not exist", func(t *testing.T) {
_, err := SelectVersion(
makePluginWithVersions(versionArg{Version: "version"}),
"1.1.1",
)
assert.Error(t, err)
})
t.Run("Should return error when no version supports current arch", func(t *testing.T) {
_, err := SelectVersion(
makePluginWithVersions(versionArg{Version: "version", Arch: []string{"non-existent"}}),
"",
)
assert.Error(t, err)
})
t.Run("Should return error when requested version does not support current arch", func(t *testing.T) {
_, err := SelectVersion(
makePluginWithVersions(
versionArg{Version: "2.0.0"},
versionArg{Version: "1.1.1", Arch: []string{"non-existent"}},
),
"1.1.1",
)
assert.Error(t, err)
})
t.Run("Should return latest available for arch when no version specified", func(t *testing.T) {
ver, err := SelectVersion(
makePluginWithVersions(
versionArg{Version: "2.0.0", Arch: []string{"non-existent"}},
versionArg{Version: "1.0.0"},
),
"",
)
require.NoError(t, err)
assert.Equal(t, "1.0.0", ver.Version)
})
t.Run("Should return latest version when no version specified", func(t *testing.T) {
ver, err := SelectVersion(
makePluginWithVersions(versionArg{Version: "2.0.0"}, versionArg{Version: "1.0.0"}),
"",
)
require.NoError(t, err)
assert.Equal(t, "2.0.0", ver.Version)
})
t.Run("Should return requested version", func(t *testing.T) {
ver, err := SelectVersion(
makePluginWithVersions(
versionArg{Version: "2.0.0"},
versionArg{Version: "1.0.0"},
),
"1.0.0",
)
require.NoError(t, err)
assert.Equal(t, "1.0.0", ver.Version)
})
}
func setupFakePluginsDir(t *testing.T) string {
dirname := "testdata/fake-plugins-dir"
err := os.RemoveAll(dirname)
require.NoError(t, err)
err = os.MkdirAll(dirname, 0750)
require.NoError(t, err)
t.Cleanup(func() {
err := os.RemoveAll(dirname)
assert.NoError(t, err)
})
dirname, err = filepath.Abs(dirname)
require.NoError(t, err)
return dirname
}
func skipWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping test on Windows")
}
}
type versionArg struct {
Version string
Arch []string
}
func makePluginWithVersions(versions ...versionArg) *models.Plugin {
plugin := &models.Plugin{
ID: "",
Category: "",
Versions: []models.Version{},
}
for _, version := range versions {
ver := models.Version{
Version: version.Version,
Commit: fmt.Sprintf("commit_%s", version.Version),
URL: fmt.Sprintf("url_%s", version.Version),
}
if version.Arch != nil {
ver.Arch = map[string]models.ArchMeta{}
for _, arch := range version.Arch {
ver.Arch[arch] = models.ArchMeta{
SHA256: fmt.Sprintf("sha256_%s", arch),
}
}
}
plugin.Versions = append(plugin.Versions, ver)
}
return plugin
}