The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
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.
grafana/pkg/plugins/manager/signature/manifest_test.go

873 lines
28 KiB

Plugins: Refactor Plugin Management (#40477) * add core plugin flow * add instrumentation * move func * remove cruft * support external backend plugins * refactor + clean up * remove comments * refactor loader * simplify core plugin path arg * cleanup loggers * move signature validator to plugins package * fix sig packaging * cleanup plugin model * remove unnecessary plugin field * add start+stop for pm * fix failures * add decommissioned state * export fields just to get things flowing * fix comments * set static routes * make image loading idempotent * merge with backend plugin manager * re-use funcs * reorder imports + remove unnecessary interface * add some TODOs + remove unused func * remove unused instrumentation func * simplify client usage * remove import alias * re-use backendplugin.Plugin interface * re order funcs * improve var name * fix log statements * refactor data model * add logic for dupe check during loading * cleanup state setting * refactor loader * cleanup manager interface * add rendering flow * refactor loading + init * add renderer support * fix renderer plugin * reformat imports * track errors * fix plugin signature inheritance * name param in interface * update func comment * fix func arg name * introduce class concept * remove func * fix external plugin check * apply changes from pm-experiment * fix core plugins * fix imports * rename interface * comment API interface * add support for testdata plugin * enable alerting + use correct core plugin contracts * slim manager API * fix param name * fix filter * support static routes * fix rendering * tidy rendering * get tests compiling * fix install+uninstall * start finder test * add finder test coverage * start loader tests * add test for core plugins * load core + bundled test * add test for nested plugin loading * add test files * clean interface + fix registering some core plugins * refactoring * reformat and create sub packages * simplify core plugin init * fix ctx cancel scenario * migrate initializer * remove Init() funcs * add test starter * new logger * flesh out initializer tests * refactoring * remove unused svc * refactor rendering flow * fixup loader tests * add enabled helper func * fix logger name * fix data fetchers * fix case where plugin dir doesn't exist * improve coverage + move dupe checking to loader * remove noisy debug logs * register core plugins automagically * add support for renderer in catalog * make private func + fix req validation * use interface * re-add check for renderer in catalog * tidy up from moving to auto reg core plugins * core plugin registrar * guards * copy over core plugins for test infra * all tests green * renames * propagate new interfaces * kill old manager * get compiling * tidy up * update naming * refactor manager test + cleanup * add more cases to finder test * migrate validator to field * more coverage * refactor dupe checking * add test for plugin class * add coverage for initializer * split out rendering * move * fixup tests * fix uss test * fix frontend settings * fix grafanads test * add check when checking sig errors * fix enabled map * fixup * allow manual setup of CM * rename to cloud-monitoring * remove TODO * add installer interface for testing * loader interface returns * tests passing * refactor + add more coverage * support 'stackdriver' * fix frontend settings loading * improve naming based on package name * small tidy * refactor test * fix renderer start * make cloud-monitoring plugin ID clearer * add plugin update test * add integration tests * don't break all if sig can't be calculated * add root URL check test * add more signature verification tests * update DTO name * update enabled plugins comment * update comments * fix linter * revert fe naming change * fix errors endpoint * reset error code field name * re-order test to help verify * assert -> require * pm check * add missing entry + re-order * re-check * dump icon log * verify manager contents first * reformat * apply PR feedback * apply style changes * fix one vs all loading err * improve log output * only start when no signature error * move log * rework plugin update check * fix test * fix multi loading from cfg.PluginSettings * improve log output #2 * add error abstraction to capture errors without registering a plugin * add debug log * add unsigned warning * e2e test attempt * fix logger * set home path * prevent panic * alternate * ugh.. fix home path * return renderer even if not started * make renderer plugin managed * add fallback renderer icon, update renderer badge + prevent changes when renderer is installed * fix icon loading * rollback renderer changes * use correct field * remove unneccessary block * remove newline * remove unused func * fix bundled plugins base + module fields * remove unused field since refactor * add authorizer abstraction * loader only returns plugins expected to run * fix multi log output
4 years ago
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{
plugins: New static scanner and validator, with Thema slot support (#53754) * coremodels: Convert plugin-metadata schema to a coremodel * Newer cuetsy; try quoting field name * Add slot definitions * Start sketching out pfs package * Rerun codegen with fixes, new cuetsy * Catch up dashboard with new cuetsy * Update to go1.18 * Use new vmuxers in thema * Add slot system in Go * Draft finished implementation of pfs * Collapse slot pkg into coremodel dir; add PluginInfo * Add the mux type on top of kernel * Refactor plugin generator for extensibility * Change models.cue package, numerous debugs * Bring new output to parity with old * Remove old plugin generation logic * Misc tweaking * Reintroduce generation of shared schemas * Drop back to go1.17 * Add globbing to tsconfig exclude * Introduce pfs test on existing testdata * Make most existing testdata tests pass with pfs * coremodels: Convert plugin-metadata schema to a coremodel * Newer cuetsy; try quoting field name * Add APIType control concept, regen pluginmeta * Use proper numeric types for schema fields * Make pluginmeta schema follow Go type breakdown * More decomposition into distinct types * Add test case for no plugin.json file * Fix missing ref to #Dependencies * Remove generated TS for pluginmeta * Update dependencies, rearrange go.mod * Regenerate without Model prefix * Use updated thema loader; this is now runnable * Skip app plugin with weird include * Make plugin tree extractor reusable * Split out slot lineage load/validate logic * Add myriad tests for new plugin validation failures * Add test for zip fixtures * One last run of codegen * Proper delinting * Ensure validation order is deterministic * Let there actually be sorting * Undo reliance on builtIn field (#54009) * undo builtIn reliance * fix tests Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
3 years ago
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)
}