mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
62485898e4
* grafana-cli: Fix security issue Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * grafana-cli: Add and improve tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
269 lines
8.1 KiB
Go
269 lines
8.1 KiB
Go
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
|
|
}
|