mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Refactor manifest verifier (#67218)
This commit is contained in:
parent
62587eee88
commit
aa9838bd25
@ -14,7 +14,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
@ -26,6 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
@ -36,7 +36,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
||||
@ -67,7 +66,7 @@ func TestCallResource(t *testing.T) {
|
||||
reg := registry.ProvideService()
|
||||
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()))
|
||||
srcs := sources.ProvideService(cfg, pCfg)
|
||||
ps, err := store.ProvideService(reg, srcs, l)
|
||||
require.NoError(t, err)
|
||||
|
@ -157,3 +157,7 @@ type KeyStore interface {
|
||||
GetLastUpdated(ctx context.Context) (*time.Time, error)
|
||||
SetLastUpdated(ctx context.Context) error
|
||||
}
|
||||
|
||||
type KeyRetriever interface {
|
||||
GetPublicKey(ctx context.Context, keyID string) (string, error)
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
@ -21,9 +20,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -1388,7 +1387,7 @@ func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
|
||||
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
|
||||
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
|
||||
signature.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
|
||||
signature.ProvideService(cfg, statickey.New()))
|
||||
|
||||
for _, cb := range cbs {
|
||||
cb(l)
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
@ -26,13 +25,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -119,7 +118,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
||||
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
|
||||
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()))
|
||||
srcs := sources.ProvideService(cfg, pCfg)
|
||||
ps, err := store.ProvideService(reg, srcs, l)
|
||||
require.NoError(t, err)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@ -15,13 +16,14 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/manifestverifier"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -56,28 +58,19 @@ func (m *PluginManifest) isV2() bool {
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
verifier *manifestverifier.ManifestVerifier
|
||||
mlog log.Logger
|
||||
log log.Logger
|
||||
kr plugins.KeyRetriever
|
||||
}
|
||||
|
||||
var _ plugins.SignatureCalculator = &Signature{}
|
||||
|
||||
func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *Signature {
|
||||
log := log.New("plugin.signature")
|
||||
func ProvideService(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature {
|
||||
return &Signature{
|
||||
verifier: manifestverifier.New(cfg, log, kv),
|
||||
mlog: log,
|
||||
log: log.New("plugin.signature"),
|
||||
kr: kr,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Signature) IsDisabled() bool {
|
||||
return s.verifier.IsDisabled()
|
||||
}
|
||||
|
||||
func (s *Signature) Run(ctx context.Context) error {
|
||||
return s.verifier.Run(ctx)
|
||||
}
|
||||
|
||||
// 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 (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) {
|
||||
@ -109,7 +102,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
return plugins.Signature{}, fmt.Errorf("files: %w", err)
|
||||
}
|
||||
if len(fsFiles) == 0 {
|
||||
s.mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID)
|
||||
s.log.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -118,13 +111,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
f, err := plugin.FS.Open("MANIFEST.txt")
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
s.mlog.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
s.log.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
s.mlog.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
s.log.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -134,13 +127,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
s.mlog.Warn("Failed to close plugin MANIFEST file", "err", err)
|
||||
s.log.Warn("Failed to close plugin MANIFEST file", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
byteValue, err := io.ReadAll(f)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
s.mlog.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
s.log.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
@ -148,7 +141,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
|
||||
manifest, err := s.readPluginManifest(ctx, byteValue)
|
||||
if err != nil {
|
||||
s.mlog.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
s.log.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -170,10 +163,10 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
// Validate that plugin is running within defined root URLs
|
||||
if len(manifest.RootURLs) > 0 {
|
||||
if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil {
|
||||
s.mlog.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs)
|
||||
s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs)
|
||||
return plugins.Signature{}, err
|
||||
} else if !match {
|
||||
s.mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID,
|
||||
s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID,
|
||||
"appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
@ -185,7 +178,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
|
||||
// Verify the manifest contents
|
||||
for p, hash := range manifest.Files {
|
||||
err = verifyHash(s.mlog, plugin, p, hash)
|
||||
err = verifyHash(s.log, plugin, p, hash)
|
||||
if err != nil {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
@ -215,13 +208,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
}
|
||||
|
||||
if len(unsignedFiles) > 0 {
|
||||
s.mlog.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles)
|
||||
s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
s.mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||
s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
@ -331,5 +324,25 @@ func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, bloc
|
||||
}
|
||||
}
|
||||
|
||||
return s.verifier.Verify(ctx, m.KeyID, block)
|
||||
return s.Verify(ctx, m.KeyID, block)
|
||||
}
|
||||
|
||||
func (s *Signature) Verify(ctx context.Context, keyID string, block *clearsign.Block) error {
|
||||
publicKey, err := s.kr.GetPublicKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "failed to parse public key", err)
|
||||
}
|
||||
|
||||
if _, err = openpgp.CheckDetachedSignature(keyring,
|
||||
bytes.NewBuffer(block.Bytes),
|
||||
block.ArmoredSignature.Body, &packet.Config{}); err != nil {
|
||||
return fmt.Errorf("%v: %w", "failed to check signature", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -11,11 +11,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -52,7 +51,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@ -69,7 +68,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
|
||||
t.Run("invalid manifest", func(t *testing.T) {
|
||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
||||
require.Error(t, err)
|
||||
})
|
||||
@ -107,7 +106,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
@ -161,7 +160,7 @@ func TestCalculate(t *testing.T) {
|
||||
setting.AppUrl = tc.appURL
|
||||
|
||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
@ -189,7 +188,7 @@ func TestCalculate(t *testing.T) {
|
||||
basePath := "../testdata/renderer-added-file/plugin"
|
||||
|
||||
runningWindows = true
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
@ -253,7 +252,7 @@ func TestCalculate(t *testing.T) {
|
||||
toSlash = tc.platform.toSlashFunc()
|
||||
fromSlash = tc.platform.fromSlashFunc()
|
||||
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
pfs, err := tc.fsFactory()
|
||||
require.NoError(t, err)
|
||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||
@ -721,7 +720,7 @@ func Test_validateManifest(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
||||
require.Errorf(t, err, tc.expectedErr)
|
||||
})
|
||||
|
@ -1,286 +0,0 @@
|
||||
package manifestverifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
|
||||
// Only used for getting the feature flag value
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
const publicKeySyncInterval = 10 * 24 * time.Hour // 10 days
|
||||
|
||||
// ManifestKeys is the database representation of public keys
|
||||
// used to verify plugin manifests.
|
||||
type ManifestKeys struct {
|
||||
KeyID string `json:"keyId"`
|
||||
PublicKey string `json:"public"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
type ManifestVerifier struct {
|
||||
cfg *config.Cfg
|
||||
mlog log.Logger
|
||||
|
||||
lock sync.Mutex
|
||||
cli http.Client
|
||||
kv plugins.KeyStore
|
||||
hasKeys bool
|
||||
}
|
||||
|
||||
func New(cfg *config.Cfg, mlog log.Logger, kv plugins.KeyStore) *ManifestVerifier {
|
||||
pmv := &ManifestVerifier{
|
||||
cfg: cfg,
|
||||
mlog: mlog,
|
||||
cli: makeHttpClient(),
|
||||
kv: kv,
|
||||
}
|
||||
return pmv
|
||||
}
|
||||
|
||||
// IsDisabled disables dynamic retrieval of public keys from the API server.
|
||||
func (pmv *ManifestVerifier) IsDisabled() bool {
|
||||
return pmv.cfg == nil || pmv.cfg.Features == nil || !pmv.cfg.Features.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey)
|
||||
}
|
||||
|
||||
func (pmv *ManifestVerifier) Run(ctx context.Context) error {
|
||||
// do an initial update if necessary
|
||||
err := pmv.updateKeys(ctx)
|
||||
if err != nil {
|
||||
pmv.mlog.Error("Error downloading plugin manifest keys", "error", err)
|
||||
}
|
||||
|
||||
// calculate initial send delay
|
||||
lastUpdated, err := pmv.kv.GetLastUpdated(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextSendInterval := time.Until(lastUpdated.Add(publicKeySyncInterval))
|
||||
if nextSendInterval < time.Minute {
|
||||
nextSendInterval = time.Minute
|
||||
}
|
||||
|
||||
downloadKeysTicker := time.NewTicker(nextSendInterval)
|
||||
defer downloadKeysTicker.Stop()
|
||||
|
||||
select {
|
||||
case <-downloadKeysTicker.C:
|
||||
err = pmv.updateKeys(ctx)
|
||||
if err != nil {
|
||||
pmv.mlog.Error("Error downloading plugin manifest keys", "error", err)
|
||||
}
|
||||
|
||||
if nextSendInterval != publicKeySyncInterval {
|
||||
nextSendInterval = publicKeySyncInterval
|
||||
downloadKeysTicker.Reset(nextSendInterval)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (pmv *ManifestVerifier) updateKeys(ctx context.Context) error {
|
||||
pmv.lock.Lock()
|
||||
defer pmv.lock.Unlock()
|
||||
|
||||
lastUpdated, err := pmv.kv.GetLastUpdated(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Since(*lastUpdated) < publicKeySyncInterval {
|
||||
// Cache is still valid
|
||||
return nil
|
||||
}
|
||||
|
||||
return pmv.downloadKeys(ctx)
|
||||
}
|
||||
|
||||
const publicKeyID = "7e4d0c6a708866e7"
|
||||
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z
|
||||
HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd
|
||||
5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT
|
||||
YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ
|
||||
EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw
|
||||
iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ
|
||||
Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo
|
||||
NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc
|
||||
1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT
|
||||
cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA
|
||||
YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv
|
||||
omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ
|
||||
fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby
|
||||
KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/
|
||||
x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn
|
||||
N1c5v9v/4h6qeA==
|
||||
=DNbR
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
// Retrieve the key from the API and store it in the database
|
||||
func (pmv *ManifestVerifier) downloadKeys(ctx context.Context) error {
|
||||
var data struct {
|
||||
Items []ManifestKeys
|
||||
}
|
||||
|
||||
url, err := url.JoinPath(pmv.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := pmv.cli.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := resp.Body.Close()
|
||||
if err != nil {
|
||||
pmv.mlog.Warn("error closing response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data.Items) == 0 {
|
||||
return errors.New("missing public key")
|
||||
}
|
||||
|
||||
cachedKeys, err := pmv.kv.ListKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shouldKeep := make(map[string]bool)
|
||||
for _, key := range data.Items {
|
||||
err = pmv.kv.Set(ctx, key.KeyID, key.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldKeep[key.KeyID] = true
|
||||
}
|
||||
|
||||
// Delete keys that are no longer in the API
|
||||
for _, key := range cachedKeys {
|
||||
if !shouldKeep[key] {
|
||||
err = pmv.kv.Del(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last updated timestamp
|
||||
return pmv.kv.SetLastUpdated(ctx)
|
||||
}
|
||||
|
||||
func (pmv *ManifestVerifier) ensureKeys(ctx context.Context) error {
|
||||
if pmv.hasKeys {
|
||||
return nil
|
||||
}
|
||||
keys, err := pmv.kv.ListKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
// Populate with the default key
|
||||
err := pmv.kv.Set(ctx, publicKeyID, publicKeyText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pmv.hasKeys = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPublicKey loads public keys from:
|
||||
// - The hard-coded value if the feature flag is not enabled.
|
||||
// - A cached value from kv storage if it has been already retrieved. This cache is populated from the grafana.com API.
|
||||
func (pmv *ManifestVerifier) getPublicKey(ctx context.Context, keyID string) (string, error) {
|
||||
if pmv.IsDisabled() {
|
||||
return publicKeyText, nil
|
||||
}
|
||||
|
||||
pmv.lock.Lock()
|
||||
defer pmv.lock.Unlock()
|
||||
|
||||
err := pmv.ensureKeys(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, exist, err := pmv.kv.Get(ctx, keyID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exist {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("missing public key for %s", keyID)
|
||||
}
|
||||
|
||||
func (pmv *ManifestVerifier) Verify(ctx context.Context, keyID string, block *clearsign.Block) error {
|
||||
publicKey, err := pmv.getPublicKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "failed to parse public key", err)
|
||||
}
|
||||
|
||||
if _, err = openpgp.CheckDetachedSignature(keyring,
|
||||
bytes.NewBuffer(block.Bytes),
|
||||
block.ArmoredSignature.Body, &packet.Config{}); err != nil {
|
||||
return fmt.Errorf("%v: %w", "failed to check signature", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Same configuration as pkg/plugins/repo/client.go
|
||||
func makeHttpClient() http.Client {
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
return http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
56
pkg/plugins/manager/signature/statickey/static_retriever.go
Normal file
56
pkg/plugins/manager/signature/statickey/static_retriever.go
Normal file
@ -0,0 +1,56 @@
|
||||
package statickey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
const publicKeyID = "7e4d0c6a708866e7"
|
||||
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z
|
||||
HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd
|
||||
5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT
|
||||
YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ
|
||||
EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw
|
||||
iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ
|
||||
Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo
|
||||
NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc
|
||||
1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT
|
||||
cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA
|
||||
YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv
|
||||
omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ
|
||||
fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby
|
||||
KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/
|
||||
x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn
|
||||
N1c5v9v/4h6qeA==
|
||||
=DNbR
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
type KeyRetriever struct{}
|
||||
|
||||
var _ plugins.KeyRetriever = (*KeyRetriever)(nil)
|
||||
|
||||
func New() *KeyRetriever {
|
||||
return &KeyRetriever{}
|
||||
}
|
||||
|
||||
func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) {
|
||||
if keyID == publicKeyID {
|
||||
return publicKeyText, nil
|
||||
}
|
||||
return "", fmt.Errorf("missing public key for %s", keyID)
|
||||
}
|
||||
|
||||
func GetDefaultKey() string {
|
||||
return publicKeyText
|
||||
}
|
||||
|
||||
func GetDefaultKeyID() string {
|
||||
return publicKeyID
|
||||
}
|
@ -8,7 +8,6 @@ import (
|
||||
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/process"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
@ -24,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
@ -51,7 +51,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
|
||||
bundleService *supportbundlesimpl.Service,
|
||||
publicDashboardsMetric *publicdashboardsmetric.Service,
|
||||
signature *signature.Signature,
|
||||
keyRetriever *dynamic.KeyRetriever,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||
@ -88,7 +88,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
loginAttemptService,
|
||||
bundleService,
|
||||
publicDashboardsMetric,
|
||||
signature,
|
||||
keyRetriever,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,234 @@
|
||||
package dynamic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
const publicKeySyncInterval = 10 * 24 * time.Hour // 10 days
|
||||
|
||||
// ManifestKeys is the database representation of public keys
|
||||
// used to verify plugin manifests.
|
||||
type ManifestKeys struct {
|
||||
KeyID string `json:"keyId"`
|
||||
PublicKey string `json:"public"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
type KeyRetriever struct {
|
||||
cfg *config.Cfg
|
||||
log log.Logger
|
||||
|
||||
lock sync.Mutex
|
||||
cli http.Client
|
||||
kv plugins.KeyStore
|
||||
hasKeys bool
|
||||
}
|
||||
|
||||
var _ plugins.KeyRetriever = (*KeyRetriever)(nil)
|
||||
|
||||
func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *KeyRetriever {
|
||||
kr := &KeyRetriever{
|
||||
cfg: cfg,
|
||||
log: log.New("plugin.signature.key_retriever"),
|
||||
cli: makeHttpClient(),
|
||||
kv: kv,
|
||||
}
|
||||
return kr
|
||||
}
|
||||
|
||||
// IsDisabled disables dynamic retrieval of public keys from the API server.
|
||||
func (kr *KeyRetriever) IsDisabled() bool {
|
||||
return !kr.cfg.Features.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey)
|
||||
}
|
||||
|
||||
func (kr *KeyRetriever) Run(ctx context.Context) error {
|
||||
// do an initial update if necessary
|
||||
err := kr.updateKeys(ctx)
|
||||
if err != nil {
|
||||
kr.log.Error("Error downloading plugin manifest keys", "error", err)
|
||||
}
|
||||
|
||||
// calculate initial send delay
|
||||
lastUpdated, err := kr.kv.GetLastUpdated(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextSendInterval := time.Until(lastUpdated.Add(publicKeySyncInterval))
|
||||
if nextSendInterval < time.Minute {
|
||||
nextSendInterval = time.Minute
|
||||
}
|
||||
|
||||
downloadKeysTicker := time.NewTicker(nextSendInterval)
|
||||
defer downloadKeysTicker.Stop()
|
||||
|
||||
select {
|
||||
case <-downloadKeysTicker.C:
|
||||
err = kr.updateKeys(ctx)
|
||||
if err != nil {
|
||||
kr.log.Error("Error downloading plugin manifest keys", "error", err)
|
||||
}
|
||||
|
||||
if nextSendInterval != publicKeySyncInterval {
|
||||
nextSendInterval = publicKeySyncInterval
|
||||
downloadKeysTicker.Reset(nextSendInterval)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (kr *KeyRetriever) updateKeys(ctx context.Context) error {
|
||||
kr.lock.Lock()
|
||||
defer kr.lock.Unlock()
|
||||
|
||||
lastUpdated, err := kr.kv.GetLastUpdated(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Since(*lastUpdated) < publicKeySyncInterval {
|
||||
// Cache is still valid
|
||||
return nil
|
||||
}
|
||||
|
||||
return kr.downloadKeys(ctx)
|
||||
}
|
||||
|
||||
// Retrieve the key from the API and store it in the database
|
||||
func (kr *KeyRetriever) downloadKeys(ctx context.Context) error {
|
||||
var data struct {
|
||||
Items []ManifestKeys
|
||||
}
|
||||
|
||||
url, err := url.JoinPath(kr.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := kr.cli.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := resp.Body.Close()
|
||||
if err != nil {
|
||||
kr.log.Warn("error closing response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data.Items) == 0 {
|
||||
return errors.New("missing public key")
|
||||
}
|
||||
|
||||
cachedKeys, err := kr.kv.ListKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shouldKeep := make(map[string]bool)
|
||||
for _, key := range data.Items {
|
||||
err = kr.kv.Set(ctx, key.KeyID, key.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldKeep[key.KeyID] = true
|
||||
}
|
||||
|
||||
// Delete keys that are no longer in the API
|
||||
for _, key := range cachedKeys {
|
||||
if !shouldKeep[key] {
|
||||
err = kr.kv.Del(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last updated timestamp
|
||||
return kr.kv.SetLastUpdated(ctx)
|
||||
}
|
||||
|
||||
func (kr *KeyRetriever) ensureKeys(ctx context.Context) error {
|
||||
if kr.hasKeys {
|
||||
return nil
|
||||
}
|
||||
keys, err := kr.kv.ListKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
// Populate with the default key
|
||||
err := kr.kv.Set(ctx, statickey.GetDefaultKeyID(), statickey.GetDefaultKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
kr.hasKeys = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPublicKey loads public keys from:
|
||||
// - The hard-coded value if the feature flag is not enabled.
|
||||
// - A cached value from kv storage if it has been already retrieved. This cache is populated from the grafana.com API.
|
||||
func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) {
|
||||
kr.lock.Lock()
|
||||
defer kr.lock.Unlock()
|
||||
|
||||
err := kr.ensureKeys(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, exist, err := kr.kv.Get(ctx, keyID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exist {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("missing public key for %s", keyID)
|
||||
}
|
||||
|
||||
// Same configuration as pkg/plugins/repo/client.go
|
||||
func makeHttpClient() http.Client {
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
return http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
@ -1,42 +1,20 @@
|
||||
package manifestverifier
|
||||
package dynamic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Verify(t *testing.T) {
|
||||
t.Run("it should verify a manifest with the default key", func(t *testing.T) {
|
||||
v := New(&config.Cfg{}, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
|
||||
body, err := os.ReadFile("../../testdata/test-app/MANIFEST.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _ := clearsign.Decode(body)
|
||||
if block == nil {
|
||||
t.Fatal("failed to decode")
|
||||
}
|
||||
|
||||
err = v.Verify(context.Background(), "7e4d0c6a708866e7", block)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.Server, chan bool) {
|
||||
done := make(chan bool)
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -64,14 +42,14 @@ func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.S
|
||||
})), done
|
||||
}
|
||||
func Test_PublicKeyUpdate(t *testing.T) {
|
||||
t.Run("it should verify a manifest with the API key", func(t *testing.T) {
|
||||
t.Run("it should retrieve an API key", func(t *testing.T) {
|
||||
cfg := &config.Cfg{
|
||||
Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...),
|
||||
}
|
||||
expectedKey := "fake"
|
||||
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
||||
cfg.GrafanaComURL = s.URL
|
||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
go func() {
|
||||
err := v.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
@ -94,7 +72,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
|
||||
expectedKey := "fake"
|
||||
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
||||
cfg.GrafanaComURL = s.URL
|
||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
go func() {
|
||||
err := v.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
@ -116,7 +94,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
|
||||
expectedKey := "fake"
|
||||
s, done := setFakeAPIServer(t, expectedKey, "other")
|
||||
cfg.GrafanaComURL = s.URL
|
||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||
go func() {
|
||||
err := v.Run(context.Background())
|
||||
require.NoError(t, err)
|
29
pkg/services/pluginsintegration/keyretriever/retriever.go
Normal file
29
pkg/services/pluginsintegration/keyretriever/retriever.go
Normal file
@ -0,0 +1,29 @@
|
||||
package keyretriever
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
||||
)
|
||||
|
||||
var _ plugins.KeyRetriever = (*Service)(nil)
|
||||
|
||||
type Service struct {
|
||||
kr plugins.KeyRetriever
|
||||
}
|
||||
|
||||
func ProvideService(dkr *dynamic.KeyRetriever) *Service {
|
||||
s := &Service{}
|
||||
if !dkr.IsDisabled() {
|
||||
s.kr = dkr
|
||||
} else {
|
||||
s.kr = statickey.New()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (kr *Service) GetPublicKey(ctx context.Context, keyID string) (string, error) {
|
||||
return kr.kr.GetPublicKey(ctx, keyID)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package keyretriever
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetPublicKey(t *testing.T) {
|
||||
t.Run("it should return a static key", func(t *testing.T) {
|
||||
cfg := &config.Cfg{
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
}
|
||||
kr := ProvideService(dynamic.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
|
||||
key, err := kr.GetPublicKey(context.Background(), statickey.GetDefaultKeyID())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, statickey.GetDefaultKey(), key)
|
||||
})
|
||||
}
|
@ -26,6 +26,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
@ -71,6 +73,9 @@ var WireSet = wire.NewSet(
|
||||
signature.ProvideService,
|
||||
wire.Bind(new(plugins.KeyStore), new(*keystore.Service)),
|
||||
keystore.ProvideService,
|
||||
wire.Bind(new(plugins.KeyRetriever), new(*keyretriever.Service)),
|
||||
keyretriever.ProvideService,
|
||||
dynamic.ProvideService,
|
||||
)
|
||||
|
||||
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
||||
|
Loading…
Reference in New Issue
Block a user