mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 04:22:13 +08:00
Plugins: Enforce signing for all plugins (#34364)
* enforce non-backend plugin signing * fix tests * add tests * add signatures * apply PR feedback * update upgrading docs
This commit is contained in:
@ -454,7 +454,11 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
|
||||
}
|
||||
|
||||
if len(scanner.errors) > 0 {
|
||||
pm.log.Warn("Some plugin scanning errors were found", "errors", scanner.errors)
|
||||
var errStr []string
|
||||
for _, err := range scanner.errors {
|
||||
errStr = append(errStr, err.Error())
|
||||
}
|
||||
pm.log.Warn("Some plugin scanning errors were found", "errors", strings.Join(errStr, ", "))
|
||||
pm.scanningErrors = scanner.errors
|
||||
}
|
||||
|
||||
@ -623,9 +627,7 @@ func (s *PluginScanner) validateSignature(plugin *plugins.PluginBase) *plugins.P
|
||||
"state", plugin.Signature)
|
||||
}
|
||||
|
||||
// For the time being, we choose to only require back-end plugins to be signed
|
||||
// NOTE: the state is calculated again when setting metadata on the object
|
||||
if !plugin.Backend || !s.requireSigned {
|
||||
if !s.requireSigned {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -633,28 +635,28 @@ func (s *PluginScanner) validateSignature(plugin *plugins.PluginBase) *plugins.P
|
||||
case plugins.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))
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin '%s' is unsigned", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureMissing,
|
||||
}
|
||||
}
|
||||
s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir",
|
||||
s.log.Warn("Running an unsigned plugin", "pluginID", plugin.Id, "pluginDir",
|
||||
plugin.PluginDir)
|
||||
return nil
|
||||
case plugins.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))
|
||||
s.log.Debug("Plugin '%s' has an invalid signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin '%s' has an invalid signature", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureInvalid,
|
||||
}
|
||||
case plugins.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))
|
||||
s.log.Debug("Plugin '%s' has a modified signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin '%s' has a modified signature", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureModified,
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature))
|
||||
panic(fmt.Sprintf("Plugin '%s' has an unrecognized plugin signature state '%s'", plugin.Id, plugin.Signature))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,17 +61,71 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", pm.apps["test-app"].Info.Screenshots[1].Path)
|
||||
})
|
||||
|
||||
t.Run("With external back-end plugin lacking signature", func(t *testing.T) {
|
||||
t.Run("With external back-end plugin lacking signature (production)", func(t *testing.T) {
|
||||
pm := createManager(t, func(pm *PluginManager) {
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned"
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned-datasource"
|
||||
pm.Cfg.Env = setting.Prod
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
const pluginID = "test"
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin '%s' is unsigned`, pluginID)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.GetDataSource(pluginID))
|
||||
assert.Nil(t, pm.GetPlugin(pluginID))
|
||||
})
|
||||
|
||||
t.Run("With external back-end plugin lacking signature (development)", func(t *testing.T) {
|
||||
pm := createManager(t, func(pm *PluginManager) {
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned-datasource"
|
||||
pm.Cfg.Env = setting.Dev
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
const pluginID = "test"
|
||||
|
||||
assert.Empty(t, pm.scanningErrors)
|
||||
assert.NotNil(t, pm.GetDataSource(pluginID))
|
||||
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
assert.NotNil(t, plugin)
|
||||
assert.Equal(t, plugins.PluginSignatureUnsigned, plugin.Signature)
|
||||
})
|
||||
|
||||
t.Run("With external panel plugin lacking signature (production)", func(t *testing.T) {
|
||||
pm := createManager(t, func(pm *PluginManager) {
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned-panel"
|
||||
pm.Cfg.Env = setting.Prod
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
const pluginID = "test-panel"
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin '%s' is unsigned`, pluginID)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.panels[pluginID])
|
||||
assert.Nil(t, pm.GetPlugin(pluginID))
|
||||
})
|
||||
|
||||
t.Run("With external panel plugin lacking signature (development)", func(t *testing.T) {
|
||||
pm := createManager(t, func(pm *PluginManager) {
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned-panel"
|
||||
pm.Cfg.Env = setting.Dev
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
pluginID := "test-panel"
|
||||
|
||||
assert.Empty(t, pm.scanningErrors)
|
||||
assert.NotNil(t, pm.panels[pluginID])
|
||||
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
assert.NotNil(t, plugin)
|
||||
assert.Equal(t, plugins.PluginSignatureUnsigned, plugin.Signature)
|
||||
})
|
||||
|
||||
t.Run("With external unsigned back-end plugin and configuration disabling signature check of this plugin", func(t *testing.T) {
|
||||
pm := createManager(t, func(pm *PluginManager) {
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned"
|
||||
pm.Cfg.PluginsPath = "testdata/unsigned-datasource"
|
||||
pm.Cfg.PluginsAllowUnsigned = []string{"test"}
|
||||
})
|
||||
err := pm.Init()
|
||||
@ -87,7 +141,10 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
|
||||
const pluginID = "test"
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin '%s' has an invalid signature`, pluginID)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.GetDataSource(pluginID))
|
||||
assert.Nil(t, pm.GetPlugin(pluginID))
|
||||
})
|
||||
|
||||
t.Run("With external back-end plugin lacking files listed in manifest", func(t *testing.T) {
|
||||
@ -99,7 +156,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin 'test' has a modified signature`)}, pm.scanningErrors)
|
||||
})
|
||||
|
||||
t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) {
|
||||
@ -221,7 +278,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin 'test' has an invalid signature`)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.plugins[("test")])
|
||||
})
|
||||
|
||||
@ -263,7 +320,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin 'test' has a modified signature`)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.plugins[("test")])
|
||||
})
|
||||
|
||||
@ -279,7 +336,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
})
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
|
||||
assert.Equal(t, []error{fmt.Errorf(`plugin 'test' has a modified signature`)}, pm.scanningErrors)
|
||||
assert.Nil(t, pm.plugins[("test")])
|
||||
})
|
||||
}
|
||||
|
28
pkg/plugins/manager/testdata/duplicate-plugins/nested/MANIFEST.txt
vendored
Normal file
28
pkg/plugins/manager/testdata/duplicate-plugins/nested/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-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1621412405893,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "e2c9f711796252bdde63b19691b248aaabea361f521fff6de8ded8d95a333609",
|
||||
"nested/plugin.json": "d4aee2052f5f9aaa3eecc90e5c5d9568efcd2d97595cd77fdcd1de0ada922638"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAmCkyjYACgkQfk0ManCIZufMrgIJAWouS5oNJixNsold48Jw
|
||||
BuCFtAT7cP0rqxiyu/Z1c06IIVcmEJg/KngcUDhP8bEN4xAunP7KfZktmGp9
|
||||
+8OqVbd/AgkBg9tWWgnMWln8XENca0ou1PTd/y24embsK3UNweqBAJPDL9el
|
||||
nnmA5UWS7pFiHQTLp/daE08o2FGclRbgHcOtFBI=
|
||||
=cSQ0
|
||||
-----END PGP SIGNATURE-----
|
27
pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/MANIFEST.txt
vendored
Normal file
27
pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/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-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1621411638406,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "d4aee2052f5f9aaa3eecc90e5c5d9568efcd2d97595cd77fdcd1de0ada922638"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAmCkxzYACgkQfk0ManCIZucKRAIJAUqsvNDA1GaHdMSQ4h+3
|
||||
lOXkvN7xMbzOpRvC3Wu7agfsNgmaQtctL/502jUpH94J6aItg7Wmx+mtvVGj
|
||||
5i456DitAgkBWDQU7KMAYRhAPNToRZhAdIBr0UXEOS6P9sM+xDuQ/gjZ2J+/
|
||||
Iy8j85zhl//0hC/RLspYVxgbZFIHmEto/y+3bbs=
|
||||
=1s40
|
||||
-----END PGP SIGNATURE-----
|
30
pkg/plugins/manager/testdata/test-app/MANIFEST.txt
vendored
Normal file
30
pkg/plugins/manager/testdata/test-app/MANIFEST.txt
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-app",
|
||||
"version": "1.0.0",
|
||||
"time": 1621356785895,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||
"dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||
"dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqAEARMKAAYFAmCj8PIACgkQfk0ManCIZueQJQII8f8za5CkX59o9A0uG/fH
|
||||
imSSFN0YxGfDXSOC1VjNwVQQWSSQ7S08cOX16cQ6huNpxUKCNUGSQnRnQIGg
|
||||
CcMMRU0CB3kZB1y8mcgq8Lcy2wiIjGhWRLmvckRSPotnyPM5hGkxQCIAX/k4
|
||||
VXEh4FM7q7uTiktRjRgrUMNXnszq4FJInkU8
|
||||
=DVFL
|
||||
-----END PGP SIGNATURE-----
|
12
pkg/plugins/manager/testdata/unsigned-panel/plugin/plugin.json
vendored
Normal file
12
pkg/plugins/manager/testdata/unsigned-panel/plugin/plugin.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Test",
|
||||
"id": "test-panel",
|
||||
"info": {
|
||||
"description": "Test panel",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user