mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Fix manifest verification (#24573)
This commit is contained in:
parent
20f0ee2f22
commit
892f9f789c
@ -55,7 +55,7 @@ type PluginStartFuncs struct {
|
||||
OnStart StartFunc
|
||||
}
|
||||
|
||||
// PluginDescriptor descriptor used for registering backend plugins.
|
||||
// PluginDescriptor is a descriptor used for registering backend plugins.
|
||||
type PluginDescriptor struct {
|
||||
pluginID string
|
||||
executablePath string
|
||||
@ -64,6 +64,11 @@ type PluginDescriptor struct {
|
||||
startFns PluginStartFuncs
|
||||
}
|
||||
|
||||
// PluginID returns the plugin ID.
|
||||
func (pd PluginDescriptor) PluginID() string {
|
||||
return pd.pluginID
|
||||
}
|
||||
|
||||
// getV2PluginSet returns list of plugins supported on v2.
|
||||
func getV2PluginSet() goplugin.PluginSet {
|
||||
return goplugin.PluginSet{
|
||||
|
@ -15,7 +15,7 @@ func (pm *PluginManager) updateAppDashboards() {
|
||||
query := models.GetPluginSettingsQuery{OrgId: 0}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
plog.Error("Failed to get all plugin settings", "error", err)
|
||||
pm.log.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package plugins
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
@ -18,7 +20,7 @@ import (
|
||||
|
||||
// Soon we can fetch keys from:
|
||||
// https://grafana.com/api/plugins/ci/keys
|
||||
var publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
@ -80,8 +82,9 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetPluginSignatureState returns the signature state for a plugin
|
||||
func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
|
||||
// getPluginSignatureState returns the signature state for a plugin.
|
||||
func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature {
|
||||
log.Debug("Getting signature state of plugin", "plugin", plugin.Id)
|
||||
manifestPath := path.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||
|
||||
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||
@ -100,9 +103,11 @@ func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
|
||||
}
|
||||
|
||||
// Verify the manifest contents
|
||||
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
|
||||
for p, hash := range manifest.Files {
|
||||
// Open the file
|
||||
f, err := os.Open(path.Join(plugin.PluginDir, p))
|
||||
fp := path.Join(plugin.PluginDir, p)
|
||||
f, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return PluginSignatureModified
|
||||
}
|
||||
@ -110,10 +115,12 @@ func GetPluginSignatureState(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
|
||||
}
|
||||
sum := string(h.Sum(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
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestManifestParsing(t *testing.T) {
|
||||
func TestReadPluginManifest(t *testing.T) {
|
||||
txt := `-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
@ -42,15 +43,14 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
manifest, err := readPluginManifest([]byte(txt))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, manifest)
|
||||
assert.Equal(t, manifest.Plugin, "grafana-googlesheets-datasource")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, manifest)
|
||||
assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin)
|
||||
})
|
||||
|
||||
t.Run("invalid manifest", func(t *testing.T) {
|
||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||
manifest, err := readPluginManifest([]byte(modified))
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, manifest)
|
||||
_, err := readPluginManifest([]byte(modified))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func (pm *PluginManager) Init() error {
|
||||
if p.IsCorePlugin {
|
||||
p.Signature = PluginSignatureInternal
|
||||
} else {
|
||||
p.Signature = GetPluginSignatureState(p)
|
||||
p.Signature = getPluginSignatureState(pm.log, p)
|
||||
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
|
||||
}
|
||||
}
|
||||
@ -282,9 +282,9 @@ func (scanner *PluginScanner) loadPlugin(pluginJsonFilePath string) error {
|
||||
pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath)
|
||||
|
||||
// For the time being, we choose to only require back-end plugins to be signed
|
||||
// NOTE: the state is calculated again for when setting metadata on the object
|
||||
// NOTE: the state is calculated again when setting metadata on the object
|
||||
if pluginCommon.Backend && scanner.requireSigned {
|
||||
sig := GetPluginSignatureState(&pluginCommon)
|
||||
sig := getPluginSignatureState(scanner.log, &pluginCommon)
|
||||
if sig != PluginSignatureValid {
|
||||
scanner.log.Debug("Invalid Plugin Signature", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir, "state", sig)
|
||||
if sig == PluginSignatureUnsigned {
|
||||
|
@ -104,6 +104,24 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
|
||||
})
|
||||
|
||||
t.Run("With external back-end plugin lacking files listed in manifest", func(t *testing.T) {
|
||||
origPluginsPath := setting.PluginsPath
|
||||
t.Cleanup(func() {
|
||||
setting.PluginsPath = origPluginsPath
|
||||
})
|
||||
setting.PluginsPath = "testdata/lacking-files"
|
||||
|
||||
fm := &fakeBackendPluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{},
|
||||
BackendPluginManager: fm,
|
||||
}
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
|
||||
})
|
||||
|
||||
t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) {
|
||||
origPluginsPath := setting.PluginsPath
|
||||
t.Cleanup(func() {
|
||||
@ -120,7 +138,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, pm.scanningErrors)
|
||||
assert.Equal(t, 0, fm.registerCount)
|
||||
assert.Empty(t, fm.registeredPlugins)
|
||||
})
|
||||
|
||||
t.Run("Transform plugins should be loaded when expressions feature is on", func(t *testing.T) {
|
||||
@ -130,18 +148,20 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
})
|
||||
setting.PluginsPath = "testdata/behind-feature-flag"
|
||||
|
||||
fm := &fakeBackendPluginManager{}
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{
|
||||
"expressions": true,
|
||||
},
|
||||
},
|
||||
BackendPluginManager: &fakeBackendPluginManager{},
|
||||
BackendPluginManager: fm,
|
||||
}
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "gel" is unsigned`)}, pm.scanningErrors)
|
||||
assert.Empty(t, pm.scanningErrors)
|
||||
assert.Equal(t, []string{"gel"}, fm.registeredPlugins)
|
||||
})
|
||||
}
|
||||
|
||||
@ -166,11 +186,11 @@ func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeBackendPluginManager struct {
|
||||
registerCount int
|
||||
registeredPlugins []string
|
||||
}
|
||||
|
||||
func (f *fakeBackendPluginManager) Register(descriptor backendplugin.PluginDescriptor) error {
|
||||
f.registerCount++
|
||||
f.registeredPlugins = append(f.registeredPlugins, descriptor.PluginID())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
23
pkg/plugins/testdata/behind-feature-flag/gel/MANIFEST.txt
vendored
Normal file
23
pkg/plugins/testdata/behind-feature-flag/gel/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"plugin": "gel",
|
||||
"version": "1.0.0",
|
||||
"files": {
|
||||
"plugin.json": "b9b3bb0dab3c4655a929a1e48a957466e3e2717992bdd29da27e5eed2fae090c"
|
||||
},
|
||||
"time": 1589274667427,
|
||||
"keyId": "7e4d0c6a708866e7"
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAl66aCsACgkQfk0ManCIZufDMAIJAWoNVihI9ZSBpUpgXrzY
|
||||
XXsI3OmHuVpzrv6M6bk5jYdzY4SyzZmdw4CB51TIDJW9SnUajlXxWLXGYY+w
|
||||
B2rSYvuhAgkBlG9w5OV3jcyg/wfUrIcCO5XRHMydCg0hIOznClzuG0uWn3wm
|
||||
d4RT/ap1ezislQ/91zvhsLgAIztZlm3EsNBv7sI=
|
||||
=WPLw
|
||||
-----END PGP SIGNATURE-----
|
@ -5,6 +5,7 @@
|
||||
"backend": true,
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
|
50
pkg/plugins/testdata/datasource-test/module.js
vendored
50
pkg/plugins/testdata/datasource-test/module.js
vendored
@ -1,50 +0,0 @@
|
||||
System.register([], function (_export) {
|
||||
"use strict";
|
||||
|
||||
return {
|
||||
setters: [],
|
||||
execute: function () {
|
||||
|
||||
function Datasource(instanceSettings, backendSrv) {
|
||||
this.url = instanceSettings.url;
|
||||
|
||||
// this.testDatasource = function() {
|
||||
// return backendSrv.datasourceRequest({
|
||||
// method: 'GET',
|
||||
// url: this.url + '/api/v4/search'
|
||||
// });
|
||||
// }
|
||||
//
|
||||
this.testDatasource = function() {
|
||||
return backendSrv.datasourceRequest({
|
||||
method: 'GET',
|
||||
url: this.url + '/tokenTest'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function ConfigCtrl() {
|
||||
|
||||
}
|
||||
|
||||
ConfigCtrl.template = `
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-13">TenantId </label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.tenantId'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-13">ClientId </label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.clientId'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-13">Client secret</label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.clientSecret'></input>
|
||||
</div>
|
||||
`;
|
||||
|
||||
_export('Datasource', Datasource);
|
||||
_export('ConfigCtrl', ConfigCtrl);
|
||||
}
|
||||
};
|
||||
});
|
31
pkg/plugins/testdata/datasource-test/plugin.json
vendored
31
pkg/plugins/testdata/datasource-test/plugin.json
vendored
@ -1,31 +0,0 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Test Datasource",
|
||||
"id": "test-ds",
|
||||
|
||||
"routes": [
|
||||
{
|
||||
"path": "tokenTest",
|
||||
"method": "*",
|
||||
"url": "https://management.azure.com",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://management.azure.com/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "api/v4/",
|
||||
"method": "*",
|
||||
"url": "http://localhost:3333",
|
||||
"headers": [
|
||||
{"name": "X-CH-Auth-API-Token", "content": "test {{.SecureJsonData.token}}"},
|
||||
{"name": "X-CH-Auth-Email", "content": "test {{.JsonData.email}}"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
24
pkg/plugins/testdata/lacking-files/plugin/MANIFEST.txt
vendored
Normal file
24
pkg/plugins/testdata/lacking-files/plugin/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"plugin": "test",
|
||||
"version": "1.0.0",
|
||||
"files": {
|
||||
"executable": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"plugin.json": "f19e18ca5dec690b94f2ff7866372db4bc63f4ff1101ee5d9061b9a6f026f6dd"
|
||||
},
|
||||
"time": 1589270570251,
|
||||
"keyId": "7e4d0c6a708866e7"
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAl66WCoACgkQfk0ManCIZuf8zwIJAWBVDJyzqchZ/DN7ZDCy
|
||||
Kyb63CajW/XdgHalPDB0sZ/80ExjCSprkFYPL+hUlTUHIXh+jGkfnYpMoqWA
|
||||
Om77bFc2AgkBV8HTsuRw/lXUezKnDuXcgUIvHvEwKWTvtbLgcuMXMDAVAEBj
|
||||
isBWmA8xvUfMzgQ9CJUHAaJ6hf1erpE4BuBqtMM=
|
||||
=uBIX
|
||||
-----END PGP SIGNATURE-----
|
14
pkg/plugins/testdata/lacking-files/plugin/plugin.json
vendored
Normal file
14
pkg/plugins/testdata/lacking-files/plugin/plugin.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Test",
|
||||
"id": "test",
|
||||
"backend": true,
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user