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/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"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/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
"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/loader/finder"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"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/sources"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"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/oauthtoken/oauthtokentest"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
"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/pluginaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
||||||
@ -67,7 +66,7 @@ func TestCallResource(t *testing.T) {
|
|||||||
reg := registry.ProvideService()
|
reg := registry.ProvideService()
|
||||||
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
||||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
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)
|
srcs := sources.ProvideService(cfg, pCfg)
|
||||||
ps, err := store.ProvideService(reg, srcs, l)
|
ps, err := store.ProvideService(reg, srcs, l)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -157,3 +157,7 @@ type KeyStore interface {
|
|||||||
GetLastUpdated(ctx context.Context) (*time.Time, error)
|
GetLastUpdated(ctx context.Context) (*time.Time, error)
|
||||||
SetLastUpdated(ctx context.Context) 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/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"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/finder"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
"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"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"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(),
|
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
|
||||||
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
|
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
|
||||||
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
|
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
|
||||||
signature.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())))
|
signature.ProvideService(cfg, statickey.New()))
|
||||||
|
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
cb(l)
|
cb(l)
|
||||||
|
@ -14,7 +14,6 @@ import (
|
|||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"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/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
"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/loader/finder"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
"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/sources"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
"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"
|
plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -119,7 +118,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
|||||||
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
|
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
|
||||||
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
||||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
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)
|
srcs := sources.ProvideService(cfg, pCfg)
|
||||||
ps, err := store.ProvideService(reg, srcs, l)
|
ps, err := store.ProvideService(reg, srcs, l)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package signature
|
package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -15,13 +16,14 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/log"
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/manifestverifier"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,28 +58,19 @@ func (m *PluginManifest) isV2() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Signature struct {
|
type Signature struct {
|
||||||
verifier *manifestverifier.ManifestVerifier
|
log log.Logger
|
||||||
mlog log.Logger
|
kr plugins.KeyRetriever
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ plugins.SignatureCalculator = &Signature{}
|
var _ plugins.SignatureCalculator = &Signature{}
|
||||||
|
|
||||||
func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *Signature {
|
func ProvideService(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature {
|
||||||
log := log.New("plugin.signature")
|
|
||||||
return &Signature{
|
return &Signature{
|
||||||
verifier: manifestverifier.New(cfg, log, kv),
|
log: log.New("plugin.signature"),
|
||||||
mlog: log,
|
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
|
// readPluginManifest attempts to read and verify the plugin manifest
|
||||||
// if any error occurs or the manifest is not valid, this will return an error
|
// 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) {
|
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)
|
return plugins.Signature{}, fmt.Errorf("files: %w", err)
|
||||||
}
|
}
|
||||||
if len(fsFiles) == 0 {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureInvalid,
|
Status: plugins.SignatureInvalid,
|
||||||
}, nil
|
}, nil
|
||||||
@ -118,13 +111,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
f, err := plugin.FS.Open("MANIFEST.txt")
|
f, err := plugin.FS.Open("MANIFEST.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureUnsigned,
|
Status: plugins.SignatureUnsigned,
|
||||||
}, nil
|
}, 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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureInvalid,
|
Status: plugins.SignatureInvalid,
|
||||||
}, nil
|
}, nil
|
||||||
@ -134,13 +127,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = f.Close(); err != nil {
|
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)
|
byteValue, err := io.ReadAll(f)
|
||||||
if err != nil || len(byteValue) < 10 {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureUnsigned,
|
Status: plugins.SignatureUnsigned,
|
||||||
}, nil
|
}, nil
|
||||||
@ -148,7 +141,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
|
|
||||||
manifest, err := s.readPluginManifest(ctx, byteValue)
|
manifest, err := s.readPluginManifest(ctx, byteValue)
|
||||||
if err != nil {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureInvalid,
|
Status: plugins.SignatureInvalid,
|
||||||
}, nil
|
}, 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
|
// Validate that plugin is running within defined root URLs
|
||||||
if len(manifest.RootURLs) > 0 {
|
if len(manifest.RootURLs) > 0 {
|
||||||
if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil {
|
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
|
return plugins.Signature{}, err
|
||||||
} else if !match {
|
} 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)
|
"appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs)
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureInvalid,
|
Status: plugins.SignatureInvalid,
|
||||||
@ -185,7 +178,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
|
|
||||||
// Verify the manifest contents
|
// Verify the manifest contents
|
||||||
for p, hash := range manifest.Files {
|
for p, hash := range manifest.Files {
|
||||||
err = verifyHash(s.mlog, plugin, p, hash)
|
err = verifyHash(s.log, plugin, p, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureModified,
|
Status: plugins.SignatureModified,
|
||||||
@ -215,13 +208,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(unsignedFiles) > 0 {
|
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{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureModified,
|
Status: plugins.SignatureModified,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
Status: plugins.SignatureValid,
|
Status: plugins.SignatureValid,
|
||||||
Type: manifest.SignatureType,
|
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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
"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"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
|||||||
-----END PGP SIGNATURE-----`
|
-----END PGP SIGNATURE-----`
|
||||||
|
|
||||||
t.Run("valid manifest", func(t *testing.T) {
|
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))
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -69,7 +68,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
|||||||
|
|
||||||
t.Run("invalid manifest", func(t *testing.T) {
|
t.Run("invalid manifest", func(t *testing.T) {
|
||||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
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))
|
_, err := s.readPluginManifest(context.Background(), []byte(modified))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
@ -107,7 +106,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
|||||||
-----END PGP SIGNATURE-----`
|
-----END PGP SIGNATURE-----`
|
||||||
|
|
||||||
t.Run("valid manifest", func(t *testing.T) {
|
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))
|
manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -161,7 +160,7 @@ func TestCalculate(t *testing.T) {
|
|||||||
setting.AppUrl = tc.appURL
|
setting.AppUrl = tc.appURL
|
||||||
|
|
||||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
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{
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||||
return plugins.External
|
return plugins.External
|
||||||
@ -189,7 +188,7 @@ func TestCalculate(t *testing.T) {
|
|||||||
basePath := "../testdata/renderer-added-file/plugin"
|
basePath := "../testdata/renderer-added-file/plugin"
|
||||||
|
|
||||||
runningWindows = true
|
runningWindows = true
|
||||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||||
return plugins.External
|
return plugins.External
|
||||||
@ -253,7 +252,7 @@ func TestCalculate(t *testing.T) {
|
|||||||
toSlash = tc.platform.toSlashFunc()
|
toSlash = tc.platform.toSlashFunc()
|
||||||
fromSlash = tc.platform.fromSlashFunc()
|
fromSlash = tc.platform.fromSlashFunc()
|
||||||
|
|
||||||
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
s := ProvideService(&config.Cfg{}, statickey.New())
|
||||||
pfs, err := tc.fsFactory()
|
pfs, err := tc.fsFactory()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||||
@ -721,7 +720,7 @@ func Test_validateManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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)
|
err := s.validateManifest(context.Background(), *tc.manifest, nil)
|
||||||
require.Errorf(t, err, tc.expectedErr)
|
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"
|
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
|
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/process"
|
"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/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"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/ngalert"
|
||||||
"github.com/grafana/grafana/pkg/services/notifications"
|
"github.com/grafana/grafana/pkg/services/notifications"
|
||||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
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"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric"
|
publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
@ -51,7 +51,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
|
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
|
||||||
bundleService *supportbundlesimpl.Service,
|
bundleService *supportbundlesimpl.Service,
|
||||||
publicDashboardsMetric *publicdashboardsmetric.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?
|
// Need to make sure these are initialized, is there a better place to put them?
|
||||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||||
@ -88,7 +88,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
bundleService,
|
bundleService,
|
||||||
publicDashboardsMetric,
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"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/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.Server, chan bool) {
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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
|
})), done
|
||||||
}
|
}
|
||||||
func Test_PublicKeyUpdate(t *testing.T) {
|
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{
|
cfg := &config.Cfg{
|
||||||
Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...),
|
Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...),
|
||||||
}
|
}
|
||||||
expectedKey := "fake"
|
expectedKey := "fake"
|
||||||
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
||||||
cfg.GrafanaComURL = s.URL
|
cfg.GrafanaComURL = s.URL
|
||||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||||
go func() {
|
go func() {
|
||||||
err := v.Run(context.Background())
|
err := v.Run(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -94,7 +72,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
|
|||||||
expectedKey := "fake"
|
expectedKey := "fake"
|
||||||
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
|
||||||
cfg.GrafanaComURL = s.URL
|
cfg.GrafanaComURL = s.URL
|
||||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||||
go func() {
|
go func() {
|
||||||
err := v.Run(context.Background())
|
err := v.Run(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -116,7 +94,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
|
|||||||
expectedKey := "fake"
|
expectedKey := "fake"
|
||||||
s, done := setFakeAPIServer(t, expectedKey, "other")
|
s, done := setFakeAPIServer(t, expectedKey, "other")
|
||||||
cfg.GrafanaComURL = s.URL
|
cfg.GrafanaComURL = s.URL
|
||||||
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore()))
|
v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
|
||||||
go func() {
|
go func() {
|
||||||
err := v.Run(context.Background())
|
err := v.Run(context.Background())
|
||||||
require.NoError(t, err)
|
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/oauthtoken"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/config"
|
"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/keystore"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
@ -71,6 +73,9 @@ var WireSet = wire.NewSet(
|
|||||||
signature.ProvideService,
|
signature.ProvideService,
|
||||||
wire.Bind(new(plugins.KeyStore), new(*keystore.Service)),
|
wire.Bind(new(plugins.KeyStore), new(*keystore.Service)),
|
||||||
keystore.ProvideService,
|
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
|
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
||||||
|
Loading…
Reference in New Issue
Block a user