mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
e2351f7951
commit
a515c54404
@ -19,25 +19,29 @@ type PluginSetting struct {
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignature `json:"signature"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignatureStatus `json:"signature"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
}
|
||||
|
||||
type PluginListItem struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
Category string `json:"category"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignature `json:"signature"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
Category string `json:"category"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignatureStatus `json:"signature"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
}
|
||||
|
||||
type PluginList []PluginListItem
|
||||
|
@ -110,6 +110,8 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
|
||||
DefaultNavUrl: pluginDef.DefaultNavUrl,
|
||||
State: pluginDef.State,
|
||||
Signature: pluginDef.Signature,
|
||||
SignatureType: pluginDef.SignatureType,
|
||||
SignatureOrg: pluginDef.SignatureOrg,
|
||||
}
|
||||
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
||||
@ -162,6 +164,8 @@ func GetPluginSettingByID(c *models.ReqContext) Response {
|
||||
HasUpdate: def.GrafanaNetHasUpdate,
|
||||
State: def.State,
|
||||
Signature: def.Signature,
|
||||
SignatureType: def.SignatureType,
|
||||
SignatureOrg: def.SignatureOrg,
|
||||
}
|
||||
|
||||
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
|
||||
|
@ -8,10 +8,13 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
@ -51,6 +54,17 @@ type pluginManifest struct {
|
||||
KeyID string `json:"keyId"`
|
||||
Time int64 `json:"time"`
|
||||
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
|
||||
@ -83,7 +97,7 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||
|
||||
@ -93,20 +107,58 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
|
||||
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
return PluginSignatureUnsigned
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifest, err := readPluginManifest(byteValue)
|
||||
if err != nil {
|
||||
log.Debug("Plugin signature invalid", "id", plugin.Id)
|
||||
return PluginSignatureInvalid
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Make sure the versions all match
|
||||
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
|
||||
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
|
||||
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.
|
||||
f, err := os.Open(fp)
|
||||
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() {
|
||||
if err := f.Close(); err != nil {
|
||||
@ -129,16 +184,42 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
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))
|
||||
if sum != hash {
|
||||
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
|
||||
log.Debug("Plugin signature valid", "id", plugin.Id)
|
||||
return PluginSignatureValid
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
SigningOrg: manifest.SignedByOrgName,
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -46,6 +47,13 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
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) {
|
||||
@ -54,3 +62,61 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
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
|
||||
}
|
||||
|
@ -21,14 +21,27 @@ var (
|
||||
PluginStateAlpha PluginState = "alpha"
|
||||
)
|
||||
|
||||
type PluginSignature string
|
||||
type PluginSignatureState struct {
|
||||
Status PluginSignatureStatus
|
||||
Type PluginSignatureType
|
||||
SigningOrg string
|
||||
}
|
||||
|
||||
type PluginSignatureStatus string
|
||||
|
||||
const (
|
||||
PluginSignatureInternal PluginSignature = "internal" // core plugin, no signature
|
||||
PluginSignatureValid PluginSignature = "valid" // signed and accurate MANIFEST
|
||||
PluginSignatureInvalid PluginSignature = "invalid" // invalid signature
|
||||
PluginSignatureModified PluginSignature = "modified" // valid signature, but content mismatch
|
||||
PluginSignatureUnsigned PluginSignature = "unsigned" // no MANIFEST file
|
||||
pluginSignatureInternal PluginSignatureStatus = "internal" // core plugin, no signature
|
||||
pluginSignatureValid PluginSignatureStatus = "valid" // signed and accurate MANIFEST
|
||||
pluginSignatureInvalid PluginSignatureStatus = "invalid" // invalid signature
|
||||
pluginSignatureModified PluginSignatureStatus = "modified" // valid signature, but content mismatch
|
||||
pluginSignatureUnsigned PluginSignatureStatus = "unsigned" // no MANIFEST file
|
||||
)
|
||||
|
||||
type PluginSignatureType string
|
||||
|
||||
const (
|
||||
grafanaType PluginSignatureType = "grafana"
|
||||
privateType PluginSignatureType = "private"
|
||||
)
|
||||
|
||||
type PluginNotFoundError struct {
|
||||
@ -62,25 +75,28 @@ type PluginLoader interface {
|
||||
|
||||
// PluginBase is the base plugin type.
|
||||
type PluginBase struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
Info PluginInfo `json:"info"`
|
||||
Dependencies PluginDependencies `json:"dependencies"`
|
||||
Includes []*PluginInclude `json:"includes"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Category string `json:"category"`
|
||||
HideFromList bool `json:"hideFromList,omitempty"`
|
||||
Preload bool `json:"preload"`
|
||||
State PluginState `json:"state,omitempty"`
|
||||
Signature PluginSignature `json:"signature"`
|
||||
Backend bool `json:"backend"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
Info PluginInfo `json:"info"`
|
||||
Dependencies PluginDependencies `json:"dependencies"`
|
||||
Includes []*PluginInclude `json:"includes"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Category string `json:"category"`
|
||||
HideFromList bool `json:"hideFromList,omitempty"`
|
||||
Preload bool `json:"preload"`
|
||||
State PluginState `json:"state,omitempty"`
|
||||
Signature PluginSignatureStatus `json:"signature"`
|
||||
Backend bool `json:"backend"`
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
DefaultNavUrl string `json:"-"`
|
||||
IsCorePlugin bool `json:"-"`
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
DefaultNavUrl string `json:"-"`
|
||||
IsCorePlugin bool `json:"-"`
|
||||
Files []string `json:"-"`
|
||||
SignatureType PluginSignatureType `json:"-"`
|
||||
SignatureOrg string `json:"-"`
|
||||
|
||||
GrafanaNetVersion string `json:"-"`
|
||||
GrafanaNetHasUpdate bool `json:"-"`
|
||||
@ -114,6 +130,8 @@ func (pb *PluginBase) registerPlugin(base *PluginBase) error {
|
||||
// Copy relevant fields from the base
|
||||
pb.PluginDir = base.PluginDir
|
||||
pb.Signature = base.Signature
|
||||
pb.SignatureType = base.SignatureType
|
||||
pb.SignatureOrg = base.SignatureOrg
|
||||
|
||||
Plugins[pb.Id] = pb
|
||||
return nil
|
||||
|
@ -145,7 +145,7 @@ func (pm *PluginManager) Init() error {
|
||||
|
||||
for _, p := range Plugins {
|
||||
if p.IsCorePlugin {
|
||||
p.Signature = PluginSignatureInternal
|
||||
p.Signature = pluginSignatureInternal
|
||||
} else {
|
||||
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.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
|
||||
|
||||
@ -383,21 +396,21 @@ func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
||||
|
||||
// validateSignature validates a plugin's signature.
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
if plugin.Root != nil {
|
||||
// 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",
|
||||
"plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin)
|
||||
} else {
|
||||
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)
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
@ -414,7 +427,7 @@ func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError {
|
||||
}
|
||||
|
||||
switch plugin.Signature {
|
||||
case PluginSignatureUnsigned:
|
||||
case pluginSignatureUnsigned:
|
||||
if allowed := s.allowUnsigned(plugin); !allowed {
|
||||
s.log.Debug("Plugin is unsigned", "id", 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",
|
||||
plugin.PluginDir)
|
||||
return nil
|
||||
case PluginSignatureInvalid:
|
||||
case pluginSignatureInvalid:
|
||||
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))
|
||||
return &PluginError{
|
||||
ErrorCode: signatureInvalid,
|
||||
}
|
||||
case PluginSignatureModified:
|
||||
case pluginSignatureModified:
|
||||
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))
|
||||
return &PluginError{
|
||||
@ -506,3 +519,23 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -89,12 +89,12 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
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
|
||||
t.Cleanup(func() {
|
||||
setting.PluginsPath = origPluginsPath
|
||||
})
|
||||
setting.PluginsPath = "testdata/invalid-signature"
|
||||
setting.PluginsPath = "testdata/invalid-v1-signature"
|
||||
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{},
|
||||
@ -158,6 +158,123 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
assert.Len(t, pm.scanningErrors, 1)
|
||||
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) {
|
||||
|
27
pkg/plugins/testdata/invalid-v2-signature-2/plugin/MANIFEST.txt
vendored
Normal file
27
pkg/plugins/testdata/invalid-v2-signature-2/plugin/MANIFEST.txt
vendored
Normal 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-----
|
0
pkg/plugins/testdata/invalid-v2-signature-2/plugin/extraFile
vendored
Normal file
0
pkg/plugins/testdata/invalid-v2-signature-2/plugin/extraFile
vendored
Normal file
16
pkg/plugins/testdata/invalid-v2-signature-2/plugin/plugin.json
vendored
Normal file
16
pkg/plugins/testdata/invalid-v2-signature-2/plugin/plugin.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
28
pkg/plugins/testdata/invalid-v2-signature/plugin/MANIFEST.txt
vendored
Normal file
28
pkg/plugins/testdata/invalid-v2-signature/plugin/MANIFEST.txt
vendored
Normal 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-----
|
16
pkg/plugins/testdata/invalid-v2-signature/plugin/plugin.json
vendored
Normal file
16
pkg/plugins/testdata/invalid-v2-signature/plugin/plugin.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
30
pkg/plugins/testdata/valid-v2-pvt-signature/plugin/MANIFEST.txt
vendored
Normal file
30
pkg/plugins/testdata/valid-v2-pvt-signature/plugin/MANIFEST.txt
vendored
Normal 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-----
|
16
pkg/plugins/testdata/valid-v2-pvt-signature/plugin/plugin.json
vendored
Normal file
16
pkg/plugins/testdata/valid-v2-pvt-signature/plugin/plugin.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
27
pkg/plugins/testdata/valid-v2-signature/plugin/MANIFEST.txt
vendored
Normal file
27
pkg/plugins/testdata/valid-v2-signature/plugin/MANIFEST.txt
vendored
Normal 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-----
|
16
pkg/plugins/testdata/valid-v2-signature/plugin/plugin.json
vendored
Normal file
16
pkg/plugins/testdata/valid-v2-signature/plugin/plugin.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user