mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
872 lines
28 KiB
872 lines
28 KiB
package signature
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
|
openpgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/config"
|
|
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
|
)
|
|
|
|
func TestReadPluginManifest(t *testing.T) {
|
|
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
|
Hash: SHA512
|
|
|
|
{
|
|
"plugin": "grafana-googlesheets-datasource",
|
|
"version": "1.0.0-dev",
|
|
"files": {
|
|
"LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d",
|
|
"README.md": "08ec6d704b6115bef57710f6d7e866c050cb50ee",
|
|
"gfx_sheets_darwin_amd64": "1b8ae92c6e80e502bb0bf2d0ae9d7223805993ab",
|
|
"gfx_sheets_linux_amd64": "f39e0cc7344d3186b1052e6d356eecaf54d75b49",
|
|
"gfx_sheets_windows_amd64.exe": "c8825dfec512c1c235244f7998ee95182f9968de",
|
|
"module.js": "aaec6f51a995b7b843b843cd14041925274d960d",
|
|
"module.js.LICENSE.txt": "7f822fe9341af8f82ad1b0c69aba957822a377cf",
|
|
"module.js.map": "c5a524f5c4237f6ed6a016d43cd46938efeadb45",
|
|
"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f"
|
|
},
|
|
"time": 1586817677115,
|
|
"keyId": "7e4d0c6a708866e7"
|
|
}
|
|
-----BEGIN PGP SIGNATURE-----
|
|
Version: OpenPGP.js v4.10.1
|
|
Comment: https://openpgpjs.org
|
|
|
|
wqEEARMKAAYFAl6U6o0ACgkQfk0ManCIZuevWAIHSvcxOy1SvvL5gC+HpYyG
|
|
VbSsUvF2FsCoXUCTQflK6VdJfSPNzm8YdCdx7gNrBdly6HEs06ZaRp44F/ve
|
|
NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
|
74uuP5UXZxwK2YKJovdSknDIU7MhfuvvQIP/og==
|
|
=hBea
|
|
-----END PGP SIGNATURE-----`
|
|
|
|
t.Run("valid manifest", func(t *testing.T) {
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, manifest)
|
|
assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin)
|
|
assert.Equal(t, "1.0.0-dev", manifest.Version)
|
|
assert.Equal(t, int64(1586817677115), manifest.Time)
|
|
assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
|
|
expectedFiles := []string{"LICENSE", "README.md", "gfx_sheets_darwin_amd64", "gfx_sheets_linux_amd64",
|
|
"gfx_sheets_windows_amd64.exe", "module.js", "module.js.LICENSE.txt", "module.js.map", "plugin.json",
|
|
}
|
|
assert.Equal(t, expectedFiles, fileList(manifest))
|
|
})
|
|
|
|
t.Run("invalid manifest", func(t *testing.T) {
|
|
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestReadPluginManifestV2(t *testing.T) {
|
|
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
|
Hash: SHA512
|
|
|
|
{
|
|
"manifestVersion": "2.0.0",
|
|
"signatureType": "private",
|
|
"signedByOrg": "willbrowne",
|
|
"signedByOrgName": "Will Browne",
|
|
"rootUrls": [
|
|
"http://localhost:3000/"
|
|
],
|
|
"plugin": "test",
|
|
"version": "1.0.0",
|
|
"time": 1605807018050,
|
|
"keyId": "7e4d0c6a708866e7",
|
|
"files": {
|
|
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
|
|
}
|
|
}
|
|
-----BEGIN PGP SIGNATURE-----
|
|
Version: OpenPGP.js v4.10.1
|
|
Comment: https://openpgpjs.org
|
|
|
|
wqIEARMKAAYFAl+2q6oACgkQfk0ManCIZudmzwIJAXWz58cd/91rTXszKPnE
|
|
xbVEvERCbjKTtPBQBNQyqEvV+Ig3MuBSNOVy2SOGrMsdbS6lONgvgt4Cm+iS
|
|
wV+vYifkAgkBJtg/9DMB7/iX5O0h49CtSltcpfBFXlGqIeOwRac/yENzRzAA
|
|
khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
|
=rLIE
|
|
-----END PGP SIGNATURE-----`
|
|
|
|
t.Run("valid manifest", func(t *testing.T) {
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, manifest)
|
|
assert.Equal(t, "test", manifest.Plugin)
|
|
assert.Equal(t, "1.0.0", manifest.Version)
|
|
assert.Equal(t, int64(1605807018050), manifest.Time)
|
|
assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
|
|
assert.Equal(t, "2.0.0", manifest.ManifestVersion)
|
|
assert.Equal(t, plugins.SignatureTypePrivate, manifest.SignatureType)
|
|
assert.Equal(t, "willbrowne", manifest.SignedByOrg)
|
|
assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
|
|
assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
|
|
assert.Equal(t, []string{"plugin.json"}, fileList(manifest))
|
|
})
|
|
}
|
|
|
|
func TestCalculate(t *testing.T) {
|
|
t.Run("Validate root URL against App URL for non-private plugin if is specified in manifest", func(t *testing.T) {
|
|
tcs := []struct {
|
|
appURL string
|
|
expectedSignature plugins.Signature
|
|
}{
|
|
{
|
|
appURL: "https://dev.grafana.com",
|
|
expectedSignature: plugins.Signature{
|
|
Status: plugins.SignatureStatusValid,
|
|
Type: plugins.SignatureTypeGrafana,
|
|
SigningOrg: "Grafana Labs",
|
|
},
|
|
},
|
|
{
|
|
appURL: "https://non.matching.url.com",
|
|
expectedSignature: plugins.Signature{
|
|
Status: plugins.SignatureStatusInvalid,
|
|
},
|
|
},
|
|
}
|
|
|
|
parentDir, err := filepath.Abs("../")
|
|
if err != nil {
|
|
t.Errorf("could not construct absolute path of current dir")
|
|
return
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
|
s := ProvideService(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}, statickey.New())
|
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
|
return plugins.ClassExternal
|
|
},
|
|
}, plugins.FoundPlugin{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-datasource",
|
|
Info: plugins.Info{
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
FS: mustNewStaticFSForTests(t, basePath),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expectedSignature, sig)
|
|
}
|
|
})
|
|
|
|
t.Run("Unsigned Chromium file should not invalidate signature for Renderer plugin running on Windows", func(t *testing.T) {
|
|
backup := runningWindows
|
|
t.Cleanup(func() {
|
|
runningWindows = backup
|
|
})
|
|
|
|
basePath := "../testdata/renderer-added-file/plugin"
|
|
|
|
runningWindows = true
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
|
return plugins.ClassExternal
|
|
},
|
|
}, plugins.FoundPlugin{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-renderer",
|
|
Type: plugins.TypeRenderer,
|
|
Info: plugins.Info{
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
FS: mustNewStaticFSForTests(t, basePath),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, plugins.Signature{
|
|
Status: plugins.SignatureStatusValid,
|
|
Type: plugins.SignatureTypeGrafana,
|
|
SigningOrg: "Grafana Labs",
|
|
}, sig)
|
|
})
|
|
|
|
t.Run("Signature verification should work with any path separator", func(t *testing.T) {
|
|
const basePath = "../testdata/app-with-child/dist"
|
|
|
|
platformWindows := fsPlatform{separator: '\\'}
|
|
platformUnix := fsPlatform{separator: '/'}
|
|
|
|
type testCase struct {
|
|
name string
|
|
platform fsPlatform
|
|
fsFactory func() (plugins.FS, error)
|
|
}
|
|
var testCases []testCase
|
|
for _, fsFactory := range []struct {
|
|
name string
|
|
f func() (plugins.FS, error)
|
|
}{
|
|
{"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.platform.toSlashFunc()
|
|
fromSlash = tc.platform.fromSlashFunc()
|
|
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
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.ClassExternal
|
|
},
|
|
}, plugins.FoundPlugin{
|
|
JSONData: plugins.JSONData{
|
|
ID: "myorgid-simple-app",
|
|
Type: plugins.TypeApp,
|
|
Info: plugins.Info{
|
|
Version: "%VERSION%",
|
|
},
|
|
},
|
|
FS: pfs,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, plugins.Signature{
|
|
Status: plugins.SignatureStatusValid,
|
|
Type: plugins.SignatureTypeGrafana,
|
|
SigningOrg: "Grafana Labs",
|
|
}, sig)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
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 (p fsPlatform) toSlashFunc() func(string) string {
|
|
return func(path string) string {
|
|
if p.separator == '/' {
|
|
return path
|
|
}
|
|
return strings.ReplaceAll(path, string(p.separator), "/")
|
|
}
|
|
}
|
|
|
|
// 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 := fsPlatform{'/'}.toSlashFunc()
|
|
require.Equal(t, "folder", toSlashUnix("folder"))
|
|
require.Equal(t, "/folder", toSlashUnix("/folder"))
|
|
require.Equal(t, "/folder/file", toSlashUnix("/folder/file"))
|
|
require.Equal(t, "/folder/other\\file", toSlashUnix("/folder/other\\file"))
|
|
})
|
|
|
|
t.Run("windows", func(t *testing.T) {
|
|
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 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.FS
|
|
|
|
separator string
|
|
}
|
|
|
|
// newPathSeparatorOverrideFS returns a new fsPathSeparatorFiles. Sep is the separator that will be used ONLY for
|
|
// the elements returned by Files().
|
|
func newPathSeparatorOverrideFS(sep string, ufs plugins.FS) (fsPathSeparatorFiles, error) {
|
|
return fsPathSeparatorFiles{
|
|
FS: ufs,
|
|
separator: sep,
|
|
}, nil
|
|
}
|
|
|
|
// 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, 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) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
sep string
|
|
}{
|
|
{"unix", "/"},
|
|
{"windows", "\\"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
pfs, err := newPathSeparatorOverrideFS(
|
|
tc.sep, 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func fileList(manifest *PluginManifest) []string {
|
|
keys := make([]string, 0, len(manifest.Files))
|
|
for k := range manifest.Files {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
}
|
|
|
|
func Test_urlMatch_privateGlob(t *testing.T) {
|
|
type args struct {
|
|
specs []string
|
|
target string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
shouldMatch bool
|
|
}{
|
|
{
|
|
name: "Support single wildcard matching single subdomain",
|
|
args: args{
|
|
specs: []string{"https://*.example.com"},
|
|
target: "https://test.example.com",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support single wildcard matching multiple subdomains",
|
|
args: args{
|
|
specs: []string{"https://*.example.com"},
|
|
target: "https://more.test.example.com",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support multiple wildcards matching multiple subdomains",
|
|
args: args{
|
|
specs: []string{"https://**.example.com"},
|
|
target: "https://test.example.com",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Support multiple wildcards matching multiple subdomains",
|
|
args: args{
|
|
specs: []string{"https://**.example.com"},
|
|
target: "https://more.test.example.com",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Support single wildcard matching single paths",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support single wildcard matching multiple paths",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/*"},
|
|
target: "https://www.example.com/other/grafana",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support double wildcard matching multiple paths",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/**"},
|
|
target: "https://www.example.com/other/grafana",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support subdomain mismatch",
|
|
args: args{
|
|
specs: []string{"https://www.test.example.com/grafana/docs"},
|
|
target: "https://www.dev.example.com/grafana/docs",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support single wildcard matching single path",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/grafana*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support single wildcard matching different path prefix",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/grafana*"},
|
|
target: "https://www.example.com/somethingelse",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support path mismatch",
|
|
args: args{
|
|
specs: []string{"https://example.com/grafana"},
|
|
target: "https://example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support both domain and path wildcards",
|
|
args: args{
|
|
specs: []string{"https://*.example.com/*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support wildcards without TLDs",
|
|
args: args{
|
|
specs: []string{"https://example.*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support exact match",
|
|
args: args{
|
|
specs: []string{"https://example.com/test"},
|
|
target: "https://example.com/test",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Does not support scheme mismatch",
|
|
args: args{
|
|
specs: []string{"https://test.example.com/grafana"},
|
|
target: "http://test.example.com/grafana",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Support trailing slash in spec",
|
|
args: args{
|
|
specs: []string{"https://example.com/"},
|
|
target: "https://example.com",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Support trailing slash in target",
|
|
args: args{
|
|
specs: []string{"https://example.com"},
|
|
target: "https://example.com/",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := urlMatch(tt.args.specs, tt.args.target, plugins.SignatureTypePrivateGlob)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.shouldMatch, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_urlMatch_private(t *testing.T) {
|
|
type args struct {
|
|
specs []string
|
|
target string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
shouldMatch bool
|
|
}{
|
|
{
|
|
name: "Support exact match",
|
|
args: args{
|
|
specs: []string{"https://example.com/test"},
|
|
target: "https://example.com/test",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Support trailing slash in spec",
|
|
args: args{
|
|
specs: []string{"https://example.com/test/"},
|
|
target: "https://example.com/test",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Support trailing slash in target",
|
|
args: args{
|
|
specs: []string{"https://example.com/test"},
|
|
target: "https://example.com/test/",
|
|
},
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "Do not support single wildcard matching single subdomain",
|
|
args: args{
|
|
specs: []string{"https://*.example.com"},
|
|
target: "https://test.example.com",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support multiple wildcards matching multiple subdomains",
|
|
args: args{
|
|
specs: []string{"https://**.example.com"},
|
|
target: "https://more.test.example.com",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support single wildcard matching single paths",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support double wildcard matching multiple paths",
|
|
args: args{
|
|
specs: []string{"https://www.example.com/**"},
|
|
target: "https://www.example.com/other/grafana",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support subdomain mismatch",
|
|
args: args{
|
|
specs: []string{"https://www.test.example.com/grafana/docs"},
|
|
target: "https://www.dev.example.com/grafana/docs",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support path mismatch",
|
|
args: args{
|
|
specs: []string{"https://example.com/grafana"},
|
|
target: "https://example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support both domain and path wildcards",
|
|
args: args{
|
|
specs: []string{"https://*.example.com/*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support wildcards without TLDs",
|
|
args: args{
|
|
specs: []string{"https://example.*"},
|
|
target: "https://www.example.com/grafana1",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "Do not support scheme mismatch",
|
|
args: args{
|
|
specs: []string{"https://test.example.com/grafana"},
|
|
target: "http://test.example.com/grafana",
|
|
},
|
|
shouldMatch: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := urlMatch(tt.args.specs, tt.args.target, plugins.SignatureTypePrivate)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.shouldMatch, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_validateManifest(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
manifest *PluginManifest
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Empty plugin field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }),
|
|
expectedErr: "valid manifest field plugin is required",
|
|
},
|
|
{
|
|
name: "Empty keyId field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }),
|
|
expectedErr: "valid manifest field keyId is required",
|
|
},
|
|
{
|
|
name: "Empty signedByOrg field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }),
|
|
expectedErr: "valid manifest field signedByOrg is required",
|
|
},
|
|
{
|
|
name: "Empty signedByOrgName field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }),
|
|
expectedErr: "valid manifest field SignedByOrgName is required",
|
|
},
|
|
{
|
|
name: "Empty signatureType field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }),
|
|
expectedErr: "valid manifest field signatureType is required",
|
|
},
|
|
{
|
|
name: "Invalid signatureType field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }),
|
|
expectedErr: "valid manifest field signatureType is required",
|
|
},
|
|
{
|
|
name: "Empty files field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }),
|
|
expectedErr: "valid manifest field files is required",
|
|
},
|
|
{
|
|
name: "Empty time field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }),
|
|
expectedErr: "valid manifest field time is required",
|
|
},
|
|
{
|
|
name: "Empty version field",
|
|
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }),
|
|
expectedErr: "valid manifest field version is required",
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := ProvideService(&config.PluginManagementCfg{}, statickey.New())
|
|
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
|
require.Errorf(t, err, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest {
|
|
t.Helper()
|
|
|
|
m := &PluginManifest{
|
|
Plugin: "grafana-test-app",
|
|
Version: "2.5.3",
|
|
KeyID: "7e4d0c6a708866e7",
|
|
Time: 1586817677115,
|
|
Files: map[string]string{
|
|
"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f",
|
|
},
|
|
ManifestVersion: "2.0.0",
|
|
SignatureType: plugins.SignatureTypeGrafana,
|
|
SignedByOrg: "grafana",
|
|
SignedByOrgName: "grafana",
|
|
}
|
|
|
|
for _, cb := range cbs {
|
|
cb(m)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
|
|
sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
|
|
require.NoError(t, err)
|
|
return sfs
|
|
}
|
|
|
|
type revokedKeyProvider struct{}
|
|
|
|
func (p *revokedKeyProvider) GetPublicKey(ctx context.Context, keyID string) (string, error) {
|
|
// dummy revoked key created locally
|
|
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
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
|
|
pHo=
|
|
=RnzB
|
|
-----END PGP PUBLIC KEY BLOCK-----
|
|
`
|
|
return publicKeyText, nil
|
|
}
|
|
|
|
func Test_VerifyRevokedKey(t *testing.T) {
|
|
s := ProvideService(&config.PluginManagementCfg{}, &revokedKeyProvider{})
|
|
m := createV2Manifest(t)
|
|
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
|
Hash: SHA512
|
|
|
|
{
|
|
"manifestVersion": "2.0.0",
|
|
"signatureType": "private",
|
|
"signedByOrg": "andresmartinez",
|
|
"signedByOrgName": "andresmartinez",
|
|
"rootUrls": [
|
|
"https://example.com/grafana"
|
|
],
|
|
"plugin": "myorg-withbackend-app",
|
|
"version": "1.0.0",
|
|
"time": 1684235356873,
|
|
"keyId": "CE6E5BFAC57F4B66",
|
|
"files": {
|
|
"CHANGELOG.md": "ba613d6f914b27dce9ace4d8c0cb074273c9eb6c536d8e7ac24c5ce6ae941fd0",
|
|
"gpx_app-myorg-withbackend-app_darwin_amd64": "d85169cc9c3cacf71f2b570478dc14d9db1985f97d0f6c93b9cccfe1cbb7a87b",
|
|
"img/logo.svg": "1defc6f7e585c67657bcfd8fddc599ee7dfa82f8674413f49fa274c2cd453ec6",
|
|
"plugin.json": "f74b561db5b079c610cc0025602cb489c8108b18db76f2190e690501f7f5b8c6",
|
|
"go_plugin_build_manifest": "9a85c38b762566db00468a39335eab9bf14d8a86bdf5de774feae8f5c1936d8d",
|
|
"standalone.txt": "da5961ac3dc4ffe6bd26c6b5633e37bef1815cf4beee2dea769188a1295e956c",
|
|
"gpx_app-myorg-withbackend-app_linux_amd64": "4ed9ea3a13700dc36ce2918d07d6b3d2e04e2174966b8da219890f57192b36e6",
|
|
"gpx_app-myorg-withbackend-app_linux_arm": "c9e6f2c8b3e0216d46299442ab1e32a9f4ece35b806e499bc027e8de51e13f64",
|
|
"LICENSE": "c5accbbd8546e94c34aed24afe689a617627d18eed5a6c48277e48db57c23851",
|
|
"README.md": "20268419628cb55aa86394d01012be936f70f1131cee6fcf4008aded5164b7f0",
|
|
"gpx_app-myorg-withbackend-app_windows_amd64.exe": "4106704de494fc6a82733961b119c2393b066fc6f9a32822ab8f1dbacdabe057",
|
|
"module.js.map": "9cdc252e92c0408fb39326af592720223640a1a7f1a68fea23ac3aea056bf938",
|
|
"gpx_app-myorg-withbackend-app_darwin_arm64": "ae8f2966f1100a5f32af1a87f6edda0aacec615246043211926b071136909784",
|
|
"gpx_app-myorg-withbackend-app_linux_arm64": "1ea731dbe23049e790cb51c09b18983a65819f5b3b52c2f8d787293c6f362887",
|
|
"pid.txt": "edf0f8e95323881a533842f79e808584d4f584831ab53b1887c31e683e2fd5a5",
|
|
"module.js": "061e1da2918204c34f38fcb717f1e27dacd64e5cdca64f281a7f4ebb2baf33c1"
|
|
}
|
|
}
|
|
-----BEGIN PGP SIGNATURE-----
|
|
|
|
iQGzBAEBCgAdFiEEAucR15yFWlGOwy8FuJPlo2DZEaAFAmRkuWAACgkQuJPlo2DZ
|
|
EaAILgv/YElEpqelrqga3usjPsA9EAX4f7eOUv2mG9gg5UISpEc8KXwJ3htXd/3+
|
|
W2ij9KehkLsFsCTQs+oMIqJjsJAqA1nCwYEOIJlcDcuobMMMSqdJMG6xZOq3dTX4
|
|
O2DEcybtHHRD3C59ks1wmwNdI8B5T5SCxDXGmBo4kBh1h3t7sSlE3MoSTIfE3csR
|
|
GyB05ONtLpMwc+xlyuuSjcxcNAYcbCl8ts29sj3EsXSD/Vnh06Rhex2XmVtLqog9
|
|
4ygC0fBSpWONSPW31yckdae2L+ZncJVO5LZ2gdM/+69a+v5f/TTauaM7Ts3PYUBf
|
|
FtJEv/KHDaxR+SwxDgu7iMoiKntGaozyG1oZvlsSYnzs0Y9a6fHoxJT2zNCIotP2
|
|
NPprfbHTrYiDkTN3LBksKcYLU7VO8Z1/VriGfhbgDAP3s1zwSmx4tiXdwZLmdJbC
|
|
6jDpIJL3mDBMlVxNe/cCgYGIA90YpqQe/B0X3sJwUcUNSQrvZJpgoz8k04AuKuxh
|
|
syrhBXja
|
|
=cQxQ
|
|
-----END PGP SIGNATURE-----
|
|
`
|
|
block, _ := clearsign.Decode([]byte(txt))
|
|
require.NotNil(t, block, "failed to decode block")
|
|
err := s.validateManifest(context.Background(), *m, block)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, openpgpErrors.ErrKeyRevoked)
|
|
}
|
|
|