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"`
|
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
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
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