mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: add a signature status flag (#23420)
This commit is contained in:
@@ -19,23 +19,25 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginList []PluginListItem
|
type PluginList []PluginListItem
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
|
|||||||
HasUpdate: pluginDef.GrafanaNetHasUpdate,
|
HasUpdate: pluginDef.GrafanaNetHasUpdate,
|
||||||
DefaultNavUrl: pluginDef.DefaultNavUrl,
|
DefaultNavUrl: pluginDef.DefaultNavUrl,
|
||||||
State: pluginDef.State,
|
State: pluginDef.State,
|
||||||
|
Signature: pluginDef.Signature,
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
||||||
@@ -151,6 +152,7 @@ func GetPluginSettingByID(c *models.ReqContext) Response {
|
|||||||
LatestVersion: def.GrafanaNetVersion,
|
LatestVersion: def.GrafanaNetVersion,
|
||||||
HasUpdate: def.GrafanaNetHasUpdate,
|
HasUpdate: def.GrafanaNetHasUpdate,
|
||||||
State: def.State,
|
State: def.State,
|
||||||
|
Signature: def.Signature,
|
||||||
}
|
}
|
||||||
|
|
||||||
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
|
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
|
||||||
|
|||||||
114
pkg/plugins/manifest.go
Normal file
114
pkg/plugins/manifest.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Soon we can fetch keys from:
|
||||||
|
// https://grafana.com/api/plugins/ci/keys
|
||||||
|
var publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Version: OpenPGP.js v4.10.1
|
||||||
|
Comment: https://openpgpjs.org
|
||||||
|
|
||||||
|
xjMEXo5V+RYJKwYBBAHaRw8BAQdAxIzDC0767A5eOHESiU8ACz5c9BWIrkbJ
|
||||||
|
/5a4m/zsFWnNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ4BBAWCgAg
|
||||||
|
BQJejlX5BgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQ1uNw7xqtn452
|
||||||
|
hQD+LK/+1k5vdVVQDxRDyjN3+6Wiy/jK2wwH1JtHdnTUKKsA/iot3glN57wb
|
||||||
|
gaIQgQSZaE5E9tsIhGYhhNi8R743Oh4GzjgEXo5V+RIKKwYBBAGXVQEFAQEH
|
||||||
|
QCmdY+K50okUPp1NCFJxdje+Icr859fTwwRy9+hq+vUIAwEIB8JhBBgWCAAJ
|
||||||
|
BQJejlX5AhsMAAoJENbjcO8arZ+OpMwBAIcGCY1jMPo64h9G4MmFyPjL+wxn
|
||||||
|
U2YVAvfHQZnN+gD3AP47klt0/0tmSlbNwEvimZxA3tpUfNrtUO1K4E8VxSIn
|
||||||
|
Dg==
|
||||||
|
=PA1c
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
`
|
||||||
|
|
||||||
|
// PluginManifest holds details for the file manifest
|
||||||
|
type PluginManifest struct {
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
KeyID string `json:"keyId"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
Files map[string]string `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPluginManifest attempts to read and verify the plugin manifest
|
||||||
|
// if any error occurs or the manifest is not valid, this will return an error
|
||||||
|
func readPluginManifest(body []byte) (*PluginManifest, error) {
|
||||||
|
fmt.Printf("TODO... verify: %s", publicKeyText)
|
||||||
|
// block, _ := clearsign.Decode(body)
|
||||||
|
// if block == nil {
|
||||||
|
// return nil, fmt.Errorf("unable to decode manifest")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// txt := string(block.Plaintext)
|
||||||
|
// fmt.Printf("PLAINTEXT: %s", txt)
|
||||||
|
|
||||||
|
// // Convert to a well typed object
|
||||||
|
// manifest := &PluginManifest{}
|
||||||
|
// err := json.Unmarshal(block.Plaintext, &manifest)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, fmt.Errorf("Error parsing manifest JSON: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, fmt.Errorf("failed to parse public key: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if _, err := openpgp.CheckDetachedSignature(keyring,
|
||||||
|
// bytes.NewBuffer(block.Bytes),
|
||||||
|
// block.ArmoredSignature.Body); err != nil {
|
||||||
|
// return nil, fmt.Errorf("failed to check signature: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return manifest, nil
|
||||||
|
return nil, fmt.Errorf("not yet parsing the manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginSignatureState returns the signature state for a plugin
|
||||||
|
func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
|
||||||
|
manifestPath := path.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||||
|
|
||||||
|
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||||
|
if err != nil || len(byteValue) < 10 {
|
||||||
|
return PluginSignatureUnsigned
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := readPluginManifest(byteValue)
|
||||||
|
if err != nil {
|
||||||
|
return PluginSignatureInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the versions all match
|
||||||
|
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
|
||||||
|
return PluginSignatureModified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the manifest contents
|
||||||
|
for p, hash := range manifest.Files {
|
||||||
|
// Open the file
|
||||||
|
f, err := os.Open(path.Join(plugin.PluginDir, p))
|
||||||
|
if err != nil {
|
||||||
|
return PluginSignatureModified
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return PluginSignatureModified
|
||||||
|
}
|
||||||
|
sum := string(h.Sum(nil))
|
||||||
|
if sum != hash {
|
||||||
|
return PluginSignatureModified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything OK
|
||||||
|
return PluginSignatureValid
|
||||||
|
}
|
||||||
53
pkg/plugins/manifest_test.go
Normal file
53
pkg/plugins/manifest_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManifestParsing(t *testing.T) {
|
||||||
|
|
||||||
|
Convey("Should validate manifest", t, func() {
|
||||||
|
txt := `
|
||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA512
|
||||||
|
|
||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d",
|
||||||
|
"README.md": "4ebed28a02dc029719296aa847bffcea8eb5b9ff",
|
||||||
|
"gfx_sheets_darwin_amd64": "4493f107eb175b085f020c1afea04614232dc0fd",
|
||||||
|
"gfx_sheets_linux_amd64": "d8b05884e3829d1389a9c0e4b79b0aba8c19ca4a",
|
||||||
|
"gfx_sheets_windows_amd64.exe": "88f33db20182e17c72c2823fe3bed87d8c45b0fd",
|
||||||
|
"img/config-page.png": "e6d8f6704dbe85d5f032d4e8ba44ebc5d4a68c43",
|
||||||
|
"img/dashboard.png": "63d79d0e0f9db21ea168324bd4e180d6892b9d2b",
|
||||||
|
"img/graph.png": "7ea6295954b24be55b27320af2074852fb088fa1",
|
||||||
|
"img/query-editor.png": "262f2bfddb004c7ce567042e8096f9e033c9b1bd",
|
||||||
|
"img/sheets.svg": "f134ab85caff88b59ea903c5491c6a08c221622f",
|
||||||
|
"module.js": "40b8c38cea260caed3cdc01d6e3c1eca483ab5c1",
|
||||||
|
"plugin.json": "bfcae42976f0feca58eed3636655bce51702d3ed"
|
||||||
|
},
|
||||||
|
"plugin": "grafana-googlesheets-datasource",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"keyId": "ABC",
|
||||||
|
"time": 1586404562862
|
||||||
|
}
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
Version: OpenPGP.js v4.10.1
|
||||||
|
Comment: https://openpgpjs.org
|
||||||
|
|
||||||
|
wl4EARYKAAYFAl6OnNMACgkQ1uNw7xqtn45r0QEAqmoB/Q5NsJZNxnM69m2A
|
||||||
|
eQhcWNyo7yxO/4NZhVvBiJkA/iXUtptWbba3aw9TSZLn95LaUjKf4YUov29r
|
||||||
|
qX6kODEP
|
||||||
|
=YjQO
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
manifest, err := readPluginManifest([]byte(txt))
|
||||||
|
|
||||||
|
// Always an error for now
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(manifest, ShouldBeNil)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,6 +24,16 @@ var (
|
|||||||
PluginStateBeta PluginState = "beta"
|
PluginStateBeta PluginState = "beta"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PluginSignature 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
|
||||||
|
)
|
||||||
|
|
||||||
type PluginNotFoundError struct {
|
type PluginNotFoundError struct {
|
||||||
PluginId string
|
PluginId string
|
||||||
}
|
}
|
||||||
@@ -49,6 +59,7 @@ type PluginBase struct {
|
|||||||
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"`
|
||||||
|
|
||||||
IncludedInAppId string `json:"-"`
|
IncludedInAppId string `json:"-"`
|
||||||
PluginDir string `json:"-"`
|
PluginDir string `json:"-"`
|
||||||
|
|||||||
@@ -122,7 +122,10 @@ func (pm *PluginManager) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range Plugins {
|
for _, p := range Plugins {
|
||||||
if !p.IsCorePlugin {
|
if p.IsCorePlugin {
|
||||||
|
p.Signature = PluginSignatureInternal
|
||||||
|
} else {
|
||||||
|
p.Signature = GetPluginSignatureState(p)
|
||||||
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
|
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user