Plugins: Add support for signature manifest V2 (#29240)

* add support for signing manifest v2

* add log and fix var name

* shorten comment

* improve comment

* remove unnecessary param

* improve naming

* reformat

* rename var

* refactor test

* remove unnecessary assert

* simplify test requirements

* add more test cases

* address feedback

* revert naming

* flip tracking missing

* fix check

* Trigger Build
This commit is contained in:
Will Browne 2020-12-11 12:57:57 +01:00 committed by GitHub
parent e2351f7951
commit a515c54404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 557 additions and 58 deletions

View File

@ -19,25 +19,29 @@ type PluginSetting struct {
JsonData map[string]interface{} `json:"jsonData"` JsonData map[string]interface{} `json:"jsonData"`
DefaultNavUrl string `json:"defaultNavUrl"` DefaultNavUrl string `json:"defaultNavUrl"`
LatestVersion string `json:"latestVersion"` LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"` HasUpdate bool `json:"hasUpdate"`
State plugins.PluginState `json:"state"` State plugins.PluginState `json:"state"`
Signature plugins.PluginSignature `json:"signature"` Signature plugins.PluginSignatureStatus `json:"signature"`
SignatureType plugins.PluginSignatureType `json:"signatureType"`
SignatureOrg string `json:"signatureOrg"`
} }
type PluginListItem struct { type PluginListItem struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Id string `json:"id"` Id string `json:"id"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
Info *plugins.PluginInfo `json:"info"` Info *plugins.PluginInfo `json:"info"`
LatestVersion string `json:"latestVersion"` LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"` HasUpdate bool `json:"hasUpdate"`
DefaultNavUrl string `json:"defaultNavUrl"` DefaultNavUrl string `json:"defaultNavUrl"`
Category string `json:"category"` Category string `json:"category"`
State plugins.PluginState `json:"state"` State plugins.PluginState `json:"state"`
Signature plugins.PluginSignature `json:"signature"` Signature plugins.PluginSignatureStatus `json:"signature"`
SignatureType plugins.PluginSignatureType `json:"signatureType"`
SignatureOrg string `json:"signatureOrg"`
} }
type PluginList []PluginListItem type PluginList []PluginListItem

View File

@ -110,6 +110,8 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
DefaultNavUrl: pluginDef.DefaultNavUrl, DefaultNavUrl: pluginDef.DefaultNavUrl,
State: pluginDef.State, State: pluginDef.State,
Signature: pluginDef.Signature, Signature: pluginDef.Signature,
SignatureType: pluginDef.SignatureType,
SignatureOrg: pluginDef.SignatureOrg,
} }
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists { if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
@ -162,6 +164,8 @@ func GetPluginSettingByID(c *models.ReqContext) Response {
HasUpdate: def.GrafanaNetHasUpdate, HasUpdate: def.GrafanaNetHasUpdate,
State: def.State, State: def.State,
Signature: def.Signature, Signature: def.Signature,
SignatureType: def.SignatureType,
SignatureOrg: def.SignatureOrg,
} }
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId} query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}

View File

@ -8,10 +8,13 @@ import (
"errors" "errors"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
@ -51,6 +54,17 @@ type pluginManifest struct {
KeyID string `json:"keyId"` KeyID string `json:"keyId"`
Time int64 `json:"time"` Time int64 `json:"time"`
Files map[string]string `json:"files"` Files map[string]string `json:"files"`
// V2 supported fields
ManifestVersion string `json:"manifestVersion"`
SignatureType PluginSignatureType `json:"signatureType"`
SignedByOrg string `json:"signedByOrg"`
SignedByOrgName string `json:"signedByOrgName"`
RootURLs []string `json:"rootUrls"`
}
func (m *pluginManifest) isV2() bool {
return strings.HasPrefix(m.ManifestVersion, "2.")
} }
// readPluginManifest attempts to read and verify the plugin manifest // readPluginManifest attempts to read and verify the plugin manifest
@ -83,7 +97,7 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
} }
// getPluginSignatureState returns the signature state for a plugin. // getPluginSignatureState returns the signature state for a plugin.
func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature { func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatureState, error) {
log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend) log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend)
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt") manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
@ -93,20 +107,58 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
byteValue, err := ioutil.ReadFile(manifestPath) byteValue, err := ioutil.ReadFile(manifestPath)
if err != nil || len(byteValue) < 10 { if err != nil || len(byteValue) < 10 {
log.Debug("Plugin is unsigned", "id", plugin.Id) log.Debug("Plugin is unsigned", "id", plugin.Id)
return PluginSignatureUnsigned return PluginSignatureState{
Status: pluginSignatureUnsigned,
}, nil
} }
manifest, err := readPluginManifest(byteValue) manifest, err := readPluginManifest(byteValue)
if err != nil { if err != nil {
log.Debug("Plugin signature invalid", "id", plugin.Id) log.Debug("Plugin signature invalid", "id", plugin.Id)
return PluginSignatureInvalid return PluginSignatureState{
Status: pluginSignatureInvalid,
}, nil
} }
// Make sure the versions all match // Make sure the versions all match
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version { if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
return PluginSignatureModified return PluginSignatureState{
Status: pluginSignatureModified,
}, nil
} }
// Validate that private is running within defined root URLs
if manifest.SignatureType == privateType {
appURL, err := url.Parse(setting.AppUrl)
if err != nil {
return PluginSignatureState{}, err
}
foundMatch := false
for _, u := range manifest.RootURLs {
rootURL, err := url.Parse(u)
if err != nil {
log.Warn("Could not parse plugin root URL", "plugin", plugin.Id, "rootUrl", rootURL)
return PluginSignatureState{}, err
}
if rootURL.Scheme == appURL.Scheme &&
rootURL.Host == appURL.Host &&
rootURL.RequestURI() == appURL.RequestURI() {
foundMatch = true
break
}
}
if !foundMatch {
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.Id, "appUrl", appURL, "rootUrls", manifest.RootURLs)
return PluginSignatureState{
Status: pluginSignatureInvalid,
}, nil
}
}
manifestFiles := make(map[string]bool, len(manifest.Files))
// Verify the manifest contents // Verify the manifest contents
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id) log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
for p, hash := range manifest.Files { for p, hash := range manifest.Files {
@ -118,7 +170,10 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
// on the manifest file for a plugin and not user input. // on the manifest file for a plugin and not user input.
f, err := os.Open(fp) f, err := os.Open(fp)
if err != nil { if err != nil {
return PluginSignatureModified log.Warn("Plugin file listed in the manifest was not found", "plugin", plugin.Id, "filename", p, "dir", plugin.PluginDir)
return PluginSignatureState{
Status: pluginSignatureModified,
}, nil
} }
defer func() { defer func() {
if err := f.Close(); err != nil { if err := f.Close(); err != nil {
@ -129,16 +184,42 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
h := sha256.New() h := sha256.New()
if _, err := io.Copy(h, f); err != nil { if _, err := io.Copy(h, f); err != nil {
log.Warn("Couldn't read plugin file", "plugin", plugin.Id, "filename", fp) log.Warn("Couldn't read plugin file", "plugin", plugin.Id, "filename", fp)
return PluginSignatureModified return PluginSignatureState{
Status: pluginSignatureModified,
}, nil
} }
sum := hex.EncodeToString(h.Sum(nil)) sum := hex.EncodeToString(h.Sum(nil))
if sum != hash { if sum != hash {
log.Warn("Plugin file's signature has been modified versus manifest", "plugin", plugin.Id, "filename", fp) log.Warn("Plugin file's signature has been modified versus manifest", "plugin", plugin.Id, "filename", fp)
return PluginSignatureModified return PluginSignatureState{
Status: pluginSignatureModified,
}, nil
}
manifestFiles[p] = true
}
if manifest.isV2() {
// Track files missing from the manifest
var unsignedFiles []string
for _, f := range plugin.Files {
if _, exists := manifestFiles[f]; !exists {
unsignedFiles = append(unsignedFiles, f)
}
}
if len(unsignedFiles) > 0 {
log.Warn("The following files were not included in the signature", "plugin", plugin.Id, "files", unsignedFiles)
return PluginSignatureState{
Status: pluginSignatureModified,
}, nil
} }
} }
// Everything OK // Everything OK
log.Debug("Plugin signature valid", "id", plugin.Id) log.Debug("Plugin signature valid", "id", plugin.Id)
return PluginSignatureValid return PluginSignatureState{
Status: pluginSignatureValid,
Type: manifest.SignatureType,
SigningOrg: manifest.SignedByOrgName,
}, nil
} }

View File

@ -1,6 +1,7 @@
package plugins package plugins
import ( import (
"sort"
"strings" "strings"
"testing" "testing"
@ -46,6 +47,13 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, manifest) require.NotNil(t, manifest)
assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin) 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) { t.Run("invalid manifest", func(t *testing.T) {
@ -54,3 +62,61 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
require.Error(t, err) 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) {
manifest, err := readPluginManifest([]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, privateType, 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 fileList(manifest *pluginManifest) []string {
var keys []string
for k := range manifest.Files {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

View File

@ -21,14 +21,27 @@ var (
PluginStateAlpha PluginState = "alpha" PluginStateAlpha PluginState = "alpha"
) )
type PluginSignature string type PluginSignatureState struct {
Status PluginSignatureStatus
Type PluginSignatureType
SigningOrg string
}
type PluginSignatureStatus string
const ( const (
PluginSignatureInternal PluginSignature = "internal" // core plugin, no signature pluginSignatureInternal PluginSignatureStatus = "internal" // core plugin, no signature
PluginSignatureValid PluginSignature = "valid" // signed and accurate MANIFEST pluginSignatureValid PluginSignatureStatus = "valid" // signed and accurate MANIFEST
PluginSignatureInvalid PluginSignature = "invalid" // invalid signature pluginSignatureInvalid PluginSignatureStatus = "invalid" // invalid signature
PluginSignatureModified PluginSignature = "modified" // valid signature, but content mismatch pluginSignatureModified PluginSignatureStatus = "modified" // valid signature, but content mismatch
PluginSignatureUnsigned PluginSignature = "unsigned" // no MANIFEST file pluginSignatureUnsigned PluginSignatureStatus = "unsigned" // no MANIFEST file
)
type PluginSignatureType string
const (
grafanaType PluginSignatureType = "grafana"
privateType PluginSignatureType = "private"
) )
type PluginNotFoundError struct { type PluginNotFoundError struct {
@ -62,25 +75,28 @@ type PluginLoader interface {
// PluginBase is the base plugin type. // PluginBase is the base plugin type.
type PluginBase struct { type PluginBase struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Id string `json:"id"` Id string `json:"id"`
Info PluginInfo `json:"info"` Info PluginInfo `json:"info"`
Dependencies PluginDependencies `json:"dependencies"` Dependencies PluginDependencies `json:"dependencies"`
Includes []*PluginInclude `json:"includes"` Includes []*PluginInclude `json:"includes"`
Module string `json:"module"` Module string `json:"module"`
BaseUrl string `json:"baseUrl"` BaseUrl string `json:"baseUrl"`
Category string `json:"category"` Category string `json:"category"`
HideFromList bool `json:"hideFromList,omitempty"` HideFromList bool `json:"hideFromList,omitempty"`
Preload bool `json:"preload"` Preload bool `json:"preload"`
State PluginState `json:"state,omitempty"` State PluginState `json:"state,omitempty"`
Signature PluginSignature `json:"signature"` Signature PluginSignatureStatus `json:"signature"`
Backend bool `json:"backend"` Backend bool `json:"backend"`
IncludedInAppId string `json:"-"` IncludedInAppId string `json:"-"`
PluginDir string `json:"-"` PluginDir string `json:"-"`
DefaultNavUrl string `json:"-"` DefaultNavUrl string `json:"-"`
IsCorePlugin bool `json:"-"` IsCorePlugin bool `json:"-"`
Files []string `json:"-"`
SignatureType PluginSignatureType `json:"-"`
SignatureOrg string `json:"-"`
GrafanaNetVersion string `json:"-"` GrafanaNetVersion string `json:"-"`
GrafanaNetHasUpdate bool `json:"-"` GrafanaNetHasUpdate bool `json:"-"`
@ -114,6 +130,8 @@ func (pb *PluginBase) registerPlugin(base *PluginBase) error {
// Copy relevant fields from the base // Copy relevant fields from the base
pb.PluginDir = base.PluginDir pb.PluginDir = base.PluginDir
pb.Signature = base.Signature pb.Signature = base.Signature
pb.SignatureType = base.SignatureType
pb.SignatureOrg = base.SignatureOrg
Plugins[pb.Id] = pb Plugins[pb.Id] = pb
return nil return nil

View File

@ -145,7 +145,7 @@ func (pm *PluginManager) Init() error {
for _, p := range Plugins { for _, p := range Plugins {
if p.IsCorePlugin { if p.IsCorePlugin {
p.Signature = PluginSignatureInternal p.Signature = pluginSignatureInternal
} else { } else {
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version) metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
} }
@ -370,7 +370,20 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
} }
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath) pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
pluginCommon.Signature = getPluginSignatureState(s.log, &pluginCommon) pluginCommon.Files, err = collectPluginFilesWithin(pluginCommon.PluginDir)
if err != nil {
s.log.Warn("Could not collect plugin file information in directory", "pluginID", pluginCommon.Id, "dir", pluginCommon.PluginDir)
return err
}
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
if err != nil {
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
return err
}
pluginCommon.Signature = signatureState.Status
pluginCommon.SignatureType = signatureState.Type
pluginCommon.SignatureOrg = signatureState.SigningOrg
s.plugins[currentDir] = &pluginCommon s.plugins[currentDir] = &pluginCommon
@ -383,21 +396,21 @@ func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
// validateSignature validates a plugin's signature. // validateSignature validates a plugin's signature.
func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError { func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError {
if plugin.Signature == PluginSignatureValid { if plugin.Signature == pluginSignatureValid {
s.log.Debug("Plugin has valid signature", "id", plugin.Id) s.log.Debug("Plugin has valid signature", "id", plugin.Id)
return nil return nil
} }
if plugin.Root != nil { if plugin.Root != nil {
// If a descendant plugin with invalid signature, set signature to that of root // If a descendant plugin with invalid signature, set signature to that of root
if plugin.IsCorePlugin || plugin.Signature == PluginSignatureInternal { if plugin.IsCorePlugin || plugin.Signature == pluginSignatureInternal {
s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal", s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal",
"plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin) "plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin)
} else { } else {
s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id, s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id,
"root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature) "root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature)
plugin.Signature = plugin.Root.Signature plugin.Signature = plugin.Root.Signature
if plugin.Signature == PluginSignatureValid { if plugin.Signature == pluginSignatureValid {
s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id) s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id)
return nil return nil
} }
@ -414,7 +427,7 @@ func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError {
} }
switch plugin.Signature { switch plugin.Signature {
case PluginSignatureUnsigned: case pluginSignatureUnsigned:
if allowed := s.allowUnsigned(plugin); !allowed { if allowed := s.allowUnsigned(plugin); !allowed {
s.log.Debug("Plugin is unsigned", "id", plugin.Id) s.log.Debug("Plugin is unsigned", "id", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id)) s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id))
@ -425,13 +438,13 @@ func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError {
s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir", s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir",
plugin.PluginDir) plugin.PluginDir)
return nil return nil
case PluginSignatureInvalid: case pluginSignatureInvalid:
s.log.Debug("Plugin %q has an invalid signature", plugin.Id) s.log.Debug("Plugin %q has an invalid signature", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id)) s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id))
return &PluginError{ return &PluginError{
ErrorCode: signatureInvalid, ErrorCode: signatureInvalid,
} }
case PluginSignatureModified: case pluginSignatureModified:
s.log.Debug("Plugin %q has a modified signature", plugin.Id) s.log.Debug("Plugin %q has a modified signature", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id)) s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id))
return &PluginError{ return &PluginError{
@ -506,3 +519,23 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
} }
return data, nil return data, nil
} }
// gets plugin filenames that require verification for plugin signing
func collectPluginFilesWithin(rootDir string) ([]string, error) {
var files []string
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() != "MANIFEST.txt" {
file, err := filepath.Rel(rootDir, path)
if err != nil {
return err
}
files = append(files, file)
}
return nil
})
return files, err
}

View File

@ -89,12 +89,12 @@ func TestPluginManager_Init(t *testing.T) {
assert.Empty(t, pm.scanningErrors) assert.Empty(t, pm.scanningErrors)
}) })
t.Run("With external back-end plugin with invalid signature", func(t *testing.T) { t.Run("With external back-end plugin with invalid v1 signature", func(t *testing.T) {
origPluginsPath := setting.PluginsPath origPluginsPath := setting.PluginsPath
t.Cleanup(func() { t.Cleanup(func() {
setting.PluginsPath = origPluginsPath setting.PluginsPath = origPluginsPath
}) })
setting.PluginsPath = "testdata/invalid-signature" setting.PluginsPath = "testdata/invalid-v1-signature"
pm := &PluginManager{ pm := &PluginManager{
Cfg: &setting.Cfg{}, Cfg: &setting.Cfg{},
@ -158,6 +158,123 @@ func TestPluginManager_Init(t *testing.T) {
assert.Len(t, pm.scanningErrors, 1) assert.Len(t, pm.scanningErrors, 1)
assert.True(t, errors.Is(pm.scanningErrors[0], duplicatePluginError{})) assert.True(t, errors.Is(pm.scanningErrors[0], duplicatePluginError{}))
}) })
t.Run("With external back-end plugin with valid v2 signature", func(t *testing.T) {
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.PluginsPath = origPluginsPath
})
setting.PluginsPath = "testdata/valid-v2-signature"
pm := &PluginManager{
Cfg: &setting.Cfg{},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
pluginId := "test"
assert.NotNil(t, Plugins[pluginId])
assert.Equal(t, "datasource", Plugins[pluginId].Type)
assert.Equal(t, "Test", Plugins[pluginId].Name)
assert.Equal(t, pluginId, Plugins[pluginId].Id)
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
assert.Equal(t, pluginSignatureValid, Plugins[pluginId].Signature)
assert.Equal(t, grafanaType, Plugins[pluginId].SignatureType)
assert.Equal(t, "Grafana Labs", Plugins[pluginId].SignatureOrg)
assert.False(t, Plugins[pluginId].IsCorePlugin)
})
t.Run("With back-end plugin with invalid v2 private signature (mismatched root URL)", func(t *testing.T) {
origAppURL := setting.AppUrl
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.AppUrl = origAppURL
setting.PluginsPath = origPluginsPath
})
setting.AppUrl = "http://localhost:1234"
setting.PluginsPath = "testdata/valid-v2-pvt-signature"
pm := &PluginManager{
Cfg: &setting.Cfg{},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
t.Run("With back-end plugin with valid v2 private signature", func(t *testing.T) {
origAppURL := setting.AppUrl
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.AppUrl = origAppURL
setting.PluginsPath = origPluginsPath
})
setting.AppUrl = "http://localhost:3000/"
setting.PluginsPath = "testdata/valid-v2-pvt-signature"
pm := &PluginManager{
Cfg: &setting.Cfg{},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
pluginId := "test"
assert.NotNil(t, Plugins[pluginId])
assert.Equal(t, "datasource", Plugins[pluginId].Type)
assert.Equal(t, "Test", Plugins[pluginId].Name)
assert.Equal(t, pluginId, Plugins[pluginId].Id)
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
assert.Equal(t, pluginSignatureValid, Plugins[pluginId].Signature)
assert.Equal(t, privateType, Plugins[pluginId].SignatureType)
assert.Equal(t, "Will Browne", Plugins[pluginId].SignatureOrg)
assert.False(t, Plugins[pluginId].IsCorePlugin)
})
t.Run("With back-end plugin with modified v2 signature (missing file from plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.AppUrl = origAppURL
setting.PluginsPath = origPluginsPath
})
setting.AppUrl = "http://localhost:3000/"
setting.PluginsPath = "testdata/invalid-v2-signature"
pm := &PluginManager{
Cfg: &setting.Cfg{},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
t.Run("With back-end plugin with modified v2 signature (unaccounted file in plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.AppUrl = origAppURL
setting.PluginsPath = origPluginsPath
})
setting.AppUrl = "http://localhost:3000/"
setting.PluginsPath = "testdata/invalid-v2-signature-2"
pm := &PluginManager{
Cfg: &setting.Cfg{},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
} }
func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) { func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {

View File

@ -0,0 +1,27 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605807330546,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqEEARMKAAYFAl+2rOIACgkQfk0ManCIZudNOwIJAT8FTzwnRFCSLTOaR3F3
2Fh96eRbghokXcQG9WqpQAg8ZiVfGXeWWRNtV+nuQ9VOZOTO0BovWLuMkym2
ci8ABpWOAgd46LkGn3Dd8XVnGmLI6UPqHAXflItOrCMRiGcYJn5PxP1aCz8h
D0JoNI9TIKrhMtM4voU3Qhf3mIOTHueuDNS48w==
=mu2j
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605809299800,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f",
"veryImportantFile": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl+2tJMACgkQfk0ManCIZueB0AIJAT/PWs226MaIu3eDZy4o
3UH/tIExyY4zR+VSBfTS+Gji5BcIRkIn7bhM1U40KDraDCvQOl3WetgqQkPd
wcSTJJocAgkBrsrxNz/Nl+vw/usre3Funj0hPVS/6NnJXwe6sVH+gAQfeddz
MzYTY/gcUVWp8Y7l/Hg44nry0PS3sr5LQ30w/FY=
=ev+T
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}

View File

@ -0,0 +1,30 @@
-----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-----

View File

@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}

View File

@ -0,0 +1,27 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605807330546,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqEEARMKAAYFAl+2rOIACgkQfk0ManCIZudNOwIJAT8FTzwnRFCSLTOaR3F3
2Fh96eRbghokXcQG9WqpQAg8ZiVfGXeWWRNtV+nuQ9VOZOTO0BovWLuMkym2
ci8ABpWOAgd46LkGn3Dd8XVnGmLI6UPqHAXflItOrCMRiGcYJn5PxP1aCz8h
D0JoNI9TIKrhMtM4voU3Qhf3mIOTHueuDNS48w==
=mu2j
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}