mirror of
https://github.com/grafana/grafana.git
synced 2025-01-24 23:37:01 -06:00
Chore: Use Grafana API to retrieve the public key to validate plugins (#66439)
This commit is contained in:
parent
f612a72f96
commit
98c695c68f
@ -109,6 +109,7 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one |
|
||||
| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different |
|
||||
| `authenticationConfigUI` | Enables authentication configuration UI |
|
||||
| `pluginsAPIManifestKey` | Use grafana.com API to retrieve the public manifest key |
|
||||
| `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode |
|
||||
| `opensearchDetectVersion` | Enable version detection in OpenSearch |
|
||||
|
||||
|
@ -96,6 +96,7 @@ export interface FeatureToggles {
|
||||
useCachingService?: boolean;
|
||||
enableElasticsearchBackendQuerying?: boolean;
|
||||
authenticationConfigUI?: boolean;
|
||||
pluginsAPIManifestKey?: boolean;
|
||||
advancedDataSourcePicker?: boolean;
|
||||
opensearchDetectVersion?: boolean;
|
||||
}
|
||||
|
@ -60,12 +60,12 @@ func TestCallResource(t *testing.T) {
|
||||
|
||||
coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, testdatasource.ProvideService(cfg, featuremgmt.WithFeatures()), nil, nil, nil, nil, nil, nil)
|
||||
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg)
|
||||
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
reg := registry.ProvideService()
|
||||
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(), fakes.NewFakeRoleRegistry(),
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg))
|
||||
srcs := sources.ProvideService(cfg, pCfg)
|
||||
ps, err := store.ProvideService(reg, srcs, l)
|
||||
require.NoError(t, err)
|
||||
|
@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"github.com/grafana/grafana-azure-sdk-go/azsettings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -34,11 +35,15 @@ type Cfg struct {
|
||||
PluginsCDNURLTemplate string
|
||||
|
||||
Tracing Tracing
|
||||
|
||||
GrafanaComURL string
|
||||
|
||||
Features plugins.FeatureToggles
|
||||
}
|
||||
|
||||
func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
|
||||
awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings,
|
||||
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, tracing Tracing) *Cfg {
|
||||
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, tracing Tracing, features plugins.FeatureToggles) *Cfg {
|
||||
return &Cfg{
|
||||
log: log.New("plugin.cfg"),
|
||||
PluginsPath: pluginsPath,
|
||||
@ -53,5 +58,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti
|
||||
LogDatasourceRequests: logDatasourceRequests,
|
||||
PluginsCDNURLTemplate: pluginsCDNURLTemplate,
|
||||
Tracing: tracing,
|
||||
GrafanaComURL: "https://grafana.com",
|
||||
Features: features,
|
||||
}
|
||||
}
|
||||
|
@ -136,3 +136,11 @@ type ClientMiddlewareFunc func(next Client) Client
|
||||
func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client {
|
||||
return fn(next)
|
||||
}
|
||||
|
||||
type FeatureToggles interface {
|
||||
IsEnabled(flag string) bool
|
||||
}
|
||||
|
||||
type SignatureCalculator interface {
|
||||
Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error)
|
||||
}
|
||||
|
@ -25,44 +25,45 @@ import (
|
||||
var _ plugins.ErrorResolver = (*Loader)(nil)
|
||||
|
||||
type Loader struct {
|
||||
pluginFinder finder.Finder
|
||||
processManager process.Service
|
||||
pluginRegistry registry.Service
|
||||
roleRegistry plugins.RoleRegistry
|
||||
pluginInitializer initializer.Initializer
|
||||
signatureValidator signature.Validator
|
||||
pluginStorage storage.Manager
|
||||
assetPath *assetpath.Service
|
||||
log log.Logger
|
||||
cfg *config.Cfg
|
||||
|
||||
errs map[string]*plugins.SignatureError
|
||||
pluginFinder finder.Finder
|
||||
processManager process.Service
|
||||
pluginRegistry registry.Service
|
||||
roleRegistry plugins.RoleRegistry
|
||||
pluginInitializer initializer.Initializer
|
||||
signatureValidator signature.Validator
|
||||
signatureCalculator plugins.SignatureCalculator
|
||||
pluginStorage storage.Manager
|
||||
assetPath *assetpath.Service
|
||||
log log.Logger
|
||||
cfg *config.Cfg
|
||||
errs map[string]*plugins.SignatureError
|
||||
}
|
||||
|
||||
func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
|
||||
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder,
|
||||
roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service) *Loader {
|
||||
roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator) *Loader {
|
||||
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
|
||||
storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, assetPath,
|
||||
pluginFinder)
|
||||
pluginFinder, signatureCalculator)
|
||||
}
|
||||
|
||||
func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
|
||||
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
|
||||
processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry,
|
||||
assetPath *assetpath.Service, pluginFinder finder.Finder) *Loader {
|
||||
assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator) *Loader {
|
||||
return &Loader{
|
||||
pluginFinder: pluginFinder,
|
||||
pluginRegistry: pluginRegistry,
|
||||
pluginInitializer: initializer.New(cfg, backendProvider, license),
|
||||
signatureValidator: signature.NewValidator(authorizer),
|
||||
processManager: processManager,
|
||||
pluginStorage: pluginStorage,
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
log: log.New("plugin.loader"),
|
||||
roleRegistry: roleRegistry,
|
||||
cfg: cfg,
|
||||
assetPath: assetPath,
|
||||
pluginFinder: pluginFinder,
|
||||
pluginRegistry: pluginRegistry,
|
||||
pluginInitializer: initializer.New(cfg, backendProvider, license),
|
||||
signatureValidator: signature.NewValidator(authorizer),
|
||||
signatureCalculator: signatureCalculator,
|
||||
processManager: processManager,
|
||||
pluginStorage: pluginStorage,
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
log: log.New("plugin.loader"),
|
||||
roleRegistry: roleRegistry,
|
||||
cfg: cfg,
|
||||
assetPath: assetPath,
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,13 +78,14 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins
|
||||
|
||||
func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) {
|
||||
var loadedPlugins []*plugins.Plugin
|
||||
|
||||
for _, p := range found {
|
||||
if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists {
|
||||
l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
sig, err := signature.Calculate(ctx, l.log, src, p.Primary)
|
||||
sig, err := l.signatureCalculator.Calculate(ctx, src, p.Primary)
|
||||
if err != nil {
|
||||
l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err)
|
||||
continue
|
||||
|
@ -2,7 +2,10 @@ package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -21,8 +24,10 @@ 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/manifestverifier"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -1117,6 +1122,117 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Load_UseAPIForManifestPublicKey(t *testing.T) {
|
||||
t.Run("Load plugin using API manifest", func(t *testing.T) {
|
||||
pluginDir, err := filepath.Abs("../testdata/test-app")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of plugin dir")
|
||||
return
|
||||
}
|
||||
expected := []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Version: "1.0.0",
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/plugins/test-app/img/logo_small.png",
|
||||
Large: "public/plugins/test-app/img/logo_large.png",
|
||||
},
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
|
||||
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-connections"},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory"},
|
||||
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Slug: "nginx-panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Slug: "nginx-datasource"},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
FS: plugins.NewLocalFS(filesInDir(t, pluginDir), pluginDir),
|
||||
Class: plugins.External,
|
||||
Signature: plugins.SignatureValid,
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Module: "plugins/test-app/module",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
},
|
||||
}
|
||||
|
||||
reg := fakes.NewFakePluginRegistry()
|
||||
storage := fakes.NewFakePluginStorage()
|
||||
procPrvdr := fakes.NewFakeBackendProcessProvider()
|
||||
procMgr := fakes.NewFakeProcessManager()
|
||||
apiCalled := false
|
||||
cfg := &config.Cfg{Features: featuremgmt.WithFeatures([]interface{}{"pluginsAPIManifestKey"}...)}
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/plugins/ci/keys" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Use the hardcoded key
|
||||
k, err := manifestverifier.New(&config.Cfg{}, log.New("test")).GetPublicKey("7e4d0c6a708866e7")
|
||||
require.NoError(t, err)
|
||||
data := struct {
|
||||
Items []manifestverifier.ManifestKeys `json:"items"`
|
||||
}{
|
||||
Items: []manifestverifier.ManifestKeys{{PublicKey: k, KeyID: "7e4d0c6a708866e7"}},
|
||||
}
|
||||
b, err := json.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write(b)
|
||||
require.NoError(t, err)
|
||||
apiCalled = true
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
cfg.GrafanaComURL = s.URL
|
||||
l := newLoader(cfg, func(l *Loader) {
|
||||
l.pluginRegistry = reg
|
||||
l.pluginStorage = storage
|
||||
l.processManager = procMgr
|
||||
l.pluginInitializer = initializer.New(cfg, procPrvdr, fakes.NewFakeLicensingService())
|
||||
})
|
||||
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
},
|
||||
PluginURIsFunc: func(ctx context.Context) []string {
|
||||
return []string{pluginDir}
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, apiCalled)
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts...) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...))
|
||||
}
|
||||
|
||||
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
rootDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
@ -1435,7 +1551,7 @@ func Test_setPathsBasedOnApp(t *testing.T) {
|
||||
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.NewFakePluginStorage(),
|
||||
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder())
|
||||
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(), signature.ProvideService(cfg))
|
||||
|
||||
for _, cb := range cbs {
|
||||
cb(l)
|
||||
|
@ -111,13 +111,13 @@ func TestIntegrationPluginManager(t *testing.T) {
|
||||
|
||||
coreRegistry := coreplugin.ProvideCoreRegistry(am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf, phlare, parca)
|
||||
|
||||
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg)
|
||||
pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
reg := registry.ProvideService()
|
||||
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
|
||||
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(), fakes.NewFakeRoleRegistry(),
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
|
||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg))
|
||||
srcs := sources.ProvideService(cfg, pCfg)
|
||||
ps, err := store.ProvideService(reg, srcs, l)
|
||||
require.NoError(t, err)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@ -16,42 +15,15 @@ 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"
|
||||
)
|
||||
|
||||
// Soon we can fetch keys from:
|
||||
//
|
||||
// https://grafana.com/api/plugins/ci/keys
|
||||
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-----
|
||||
`
|
||||
|
||||
var (
|
||||
runningWindows = runtime.GOOS == "windows"
|
||||
|
||||
@ -79,9 +51,24 @@ func (m *PluginManifest) isV2() bool {
|
||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||
}
|
||||
|
||||
// ReadPluginManifest attempts to read and verify the plugin manifest
|
||||
type Signature struct {
|
||||
verifier *manifestverifier.ManifestVerifier
|
||||
mlog log.Logger
|
||||
}
|
||||
|
||||
var _ plugins.SignatureCalculator = &Signature{}
|
||||
|
||||
func ProvideService(cfg *config.Cfg) *Signature {
|
||||
log := log.New("plugin.signature")
|
||||
return &Signature{
|
||||
verifier: manifestverifier.New(cfg, log),
|
||||
mlog: log,
|
||||
}
|
||||
}
|
||||
|
||||
// readPluginManifest attempts to read and verify the plugin manifest
|
||||
// if any error occurs or the manifest is not valid, this will return an error
|
||||
func ReadPluginManifest(body []byte) (*PluginManifest, error) {
|
||||
func (s *Signature) readPluginManifest(body []byte) (*PluginManifest, error) {
|
||||
block, _ := clearsign.Decode(body)
|
||||
if block == nil {
|
||||
return nil, errors.New("unable to decode manifest")
|
||||
@ -94,20 +81,20 @@ func ReadPluginManifest(body []byte) (*PluginManifest, error) {
|
||||
return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err)
|
||||
}
|
||||
|
||||
if err = validateManifest(manifest, block); err != nil {
|
||||
if err = s.validateManifest(manifest, block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
|
||||
if len(plugin.FS.Files()) == 0 {
|
||||
mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID)
|
||||
s.mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -116,13 +103,13 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
|
||||
f, err := plugin.FS.Open("MANIFEST.txt")
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
mlog.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
s.mlog.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
mlog.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
s.mlog.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -132,21 +119,21 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
mlog.Warn("Failed to close plugin MANIFEST file", "err", err)
|
||||
s.mlog.Warn("Failed to close plugin MANIFEST file", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
byteValue, err := io.ReadAll(f)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
mlog.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
s.mlog.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifest, err := ReadPluginManifest(byteValue)
|
||||
manifest, err := s.readPluginManifest(byteValue)
|
||||
if err != nil {
|
||||
mlog.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
s.mlog.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
@ -168,10 +155,10 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
|
||||
// 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 {
|
||||
mlog.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs)
|
||||
s.mlog.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs)
|
||||
return plugins.Signature{}, err
|
||||
} else if !match {
|
||||
mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID,
|
||||
s.mlog.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,
|
||||
@ -183,7 +170,7 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
|
||||
|
||||
// Verify the manifest contents
|
||||
for p, hash := range manifest.Files {
|
||||
err = verifyHash(mlog, plugin, p, hash)
|
||||
err = verifyHash(s.mlog, plugin, p, hash)
|
||||
if err != nil {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
@ -213,13 +200,13 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
|
||||
}
|
||||
|
||||
if len(unsignedFiles) > 0 {
|
||||
mlog.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles)
|
||||
s.mlog.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||
s.mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
@ -299,7 +286,7 @@ func (r invalidFieldErr) Error() string {
|
||||
return fmt.Sprintf("valid manifest field %s is required", r.field)
|
||||
}
|
||||
|
||||
func validateManifest(m PluginManifest, block *clearsign.Block) error {
|
||||
func (s *Signature) validateManifest(m PluginManifest, block *clearsign.Block) error {
|
||||
if len(m.Plugin) == 0 {
|
||||
return invalidFieldErr{field: "plugin"}
|
||||
}
|
||||
@ -326,16 +313,6 @@ func validateManifest(m PluginManifest, block *clearsign.Block) error {
|
||||
return fmt.Errorf("%s is not a valid signature type", m.SignatureType)
|
||||
}
|
||||
}
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
|
||||
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
|
||||
return s.verifier.Verify(m.KeyID, block)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -49,7 +49,8 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
manifest, err := ReadPluginManifest([]byte(txt))
|
||||
s := ProvideService(&config.Cfg{})
|
||||
manifest, err := s.readPluginManifest([]byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, manifest)
|
||||
@ -65,7 +66,8 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
|
||||
|
||||
t.Run("invalid manifest", func(t *testing.T) {
|
||||
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
|
||||
_, err := ReadPluginManifest([]byte(modified))
|
||||
s := ProvideService(&config.Cfg{})
|
||||
_, err := s.readPluginManifest([]byte(modified))
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@ -102,7 +104,8 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
manifest, err := ReadPluginManifest([]byte(txt))
|
||||
s := ProvideService(&config.Cfg{})
|
||||
manifest, err := s.readPluginManifest([]byte(txt))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, manifest)
|
||||
@ -155,7 +158,8 @@ func TestCalculate(t *testing.T) {
|
||||
setting.AppUrl = tc.appURL
|
||||
|
||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
||||
sig, err := Calculate(context.Background(), log.NewTestLogger(), &fakes.FakePluginSource{
|
||||
s := ProvideService(&config.Cfg{})
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
},
|
||||
@ -185,7 +189,8 @@ func TestCalculate(t *testing.T) {
|
||||
basePath := "../testdata/renderer-added-file/plugin"
|
||||
|
||||
runningWindows = true
|
||||
sig, err := Calculate(context.Background(), log.NewTestLogger(), &fakes.FakePluginSource{
|
||||
s := ProvideService(&config.Cfg{})
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
},
|
||||
@ -233,7 +238,8 @@ func TestCalculate(t *testing.T) {
|
||||
|
||||
basePath := "../testdata/app-with-child/dist"
|
||||
|
||||
sig, err := Calculate(context.Background(), log.NewTestLogger(), &fakes.FakePluginSource{
|
||||
s := ProvideService(&config.Cfg{})
|
||||
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.External
|
||||
},
|
||||
@ -677,7 +683,8 @@ func Test_validateManifest(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateManifest(*tc.manifest, nil)
|
||||
s := ProvideService(&config.Cfg{})
|
||||
err := s.validateManifest(*tc.manifest, nil)
|
||||
require.Errorf(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
|
171
pkg/plugins/manager/signature/manifestverifier/verifier.go
Normal file
171
pkg/plugins/manager/signature/manifestverifier/verifier.go
Normal file
@ -0,0 +1,171 @@
|
||||
package manifestverifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
)
|
||||
|
||||
// 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
|
||||
publicKeys map[string]ManifestKeys
|
||||
}
|
||||
|
||||
func New(cfg *config.Cfg, mlog log.Logger) *ManifestVerifier {
|
||||
return &ManifestVerifier{
|
||||
cfg: cfg,
|
||||
publicKeys: map[string]ManifestKeys{},
|
||||
mlog: mlog,
|
||||
cli: makeHttpClient(),
|
||||
}
|
||||
}
|
||||
|
||||
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-----
|
||||
`
|
||||
|
||||
// getPublicKey loads public keys from:
|
||||
// - The hard-coded value if the feature flag is not enabled.
|
||||
// - A cached value from memory if it has been already retrieved.
|
||||
// - The Grafana.com API if the database is empty.
|
||||
func (pmv *ManifestVerifier) GetPublicKey(keyID string) (string, error) {
|
||||
if pmv.cfg == nil || pmv.cfg.Features == nil || !pmv.cfg.Features.IsEnabled("pluginsAPIManifestKey") {
|
||||
return publicKeyText, nil
|
||||
}
|
||||
|
||||
pmv.lock.Lock()
|
||||
defer pmv.lock.Unlock()
|
||||
|
||||
key, exist := pmv.publicKeys[keyID]
|
||||
if exist {
|
||||
return key.PublicKey, nil
|
||||
}
|
||||
|
||||
// Retrieve the key from the API and store it in the database
|
||||
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.NewRequest(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")
|
||||
}
|
||||
|
||||
for _, key := range data.Items {
|
||||
pmv.publicKeys[key.KeyID] = key
|
||||
}
|
||||
|
||||
key, exist = pmv.publicKeys[keyID]
|
||||
if exist {
|
||||
return key.PublicKey, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("missing public key for %s", keyID)
|
||||
}
|
||||
|
||||
func (pmv *ManifestVerifier) Verify(keyID string, block *clearsign.Block) error {
|
||||
publicKey, err := pmv.GetPublicKey(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,
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package manifestverifier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"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"))
|
||||
|
||||
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("7e4d0c6a708866e7", block)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("it should verify a manifest with the API key", func(t *testing.T) {
|
||||
cfg := &config.Cfg{
|
||||
Features: featuremgmt.WithFeatures([]interface{}{"pluginsAPIManifestKey"}...),
|
||||
}
|
||||
v := New(cfg, log.New("test"))
|
||||
apiCalled := false
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/plugins/ci/keys" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
data := struct {
|
||||
Items []ManifestKeys `json:"items"`
|
||||
}{
|
||||
Items: []ManifestKeys{{PublicKey: publicKeyText, KeyID: "7e4d0c6a708866e7"}},
|
||||
}
|
||||
b, err := json.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write(b)
|
||||
require.NoError(t, err)
|
||||
apiCalled = true
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
cfg.GrafanaComURL = s.URL
|
||||
|
||||
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("7e4d0c6a708866e7", block)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, apiCalled)
|
||||
})
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
)
|
||||
|
||||
@ -18,9 +19,12 @@ type Manager struct {
|
||||
log log.PrettyLogger
|
||||
}
|
||||
|
||||
func ProvideService() *Manager {
|
||||
defaultBaseURL := "https://grafana.com/api/plugins"
|
||||
return New(false, defaultBaseURL, log.NewPrettyLogger("plugin.repository"))
|
||||
func ProvideService(cfg *config.Cfg) (*Manager, error) {
|
||||
defaultBaseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(false, defaultBaseURL, log.NewPrettyLogger("plugin.repository")), nil
|
||||
}
|
||||
|
||||
func New(skipTLSVerify bool, baseURL string, logger log.PrettyLogger) *Manager {
|
||||
|
@ -518,6 +518,12 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaAuthnzSquad,
|
||||
},
|
||||
{
|
||||
Name: "pluginsAPIManifestKey",
|
||||
Description: "Use grafana.com API to retrieve the public manifest key",
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "advancedDataSourcePicker",
|
||||
Description: "Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode",
|
||||
|
@ -77,5 +77,6 @@ dataplaneFrontendFallback,alpha,@grafana/observability-metrics,false,false,false
|
||||
useCachingService,stable,@grafana/grafana-operator-experience-squad,false,false,true,false
|
||||
enableElasticsearchBackendQuerying,beta,@grafana/observability-logs,false,false,false,false
|
||||
authenticationConfigUI,alpha,@grafana/grafana-authnz-team,false,false,false,false
|
||||
pluginsAPIManifestKey,alpha,@grafana/plugins-platform-backend,false,false,false,false
|
||||
advancedDataSourcePicker,alpha,@grafana/dashboards-squad,false,false,false,true
|
||||
opensearchDetectVersion,alpha,@grafana/aws-plugins,false,false,false,true
|
||||
|
|
@ -319,6 +319,10 @@ const (
|
||||
// Enables authentication configuration UI
|
||||
FlagAuthenticationConfigUI = "authenticationConfigUI"
|
||||
|
||||
// FlagPluginsAPIManifestKey
|
||||
// Use grafana.com API to retrieve the public manifest key
|
||||
FlagPluginsAPIManifestKey = "pluginsAPIManifestKey"
|
||||
|
||||
// FlagAdvancedDataSourcePicker
|
||||
// Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode
|
||||
FlagAdvancedDataSourcePicker = "advancedDataSourcePicker"
|
||||
|
@ -5,10 +5,11 @@ import (
|
||||
"strings"
|
||||
|
||||
pCfg "github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) (*pCfg.Cfg, error) {
|
||||
func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, features *featuremgmt.FeatureManager) (*pCfg.Cfg, error) {
|
||||
plugins := settingProvider.Section("plugins")
|
||||
allowedUnsigned := grafanaCfg.PluginsAllowUnsigned
|
||||
if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 {
|
||||
@ -25,6 +26,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) (*
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new opentelemetry cfg: %w", err)
|
||||
}
|
||||
|
||||
return pCfg.NewCfg(
|
||||
settingProvider.KeyValue("", "app_mode").MustBool(grafanaCfg.Env == setting.Dev),
|
||||
grafanaCfg.PluginsPath,
|
||||
@ -38,6 +40,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) (*
|
||||
grafanaCfg.PluginLogBackendRequests,
|
||||
grafanaCfg.PluginsCDNURLTemplate,
|
||||
tracingCfg,
|
||||
featuremgmt.ProvideToggles(features),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,8 @@ var WireSet = wire.NewSet(
|
||||
wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)),
|
||||
filestore.ProvideService,
|
||||
wire.Bind(new(plugins.FileStore), new(*filestore.Service)),
|
||||
wire.Bind(new(plugins.SignatureCalculator), new(*signature.Signature)),
|
||||
signature.ProvideService,
|
||||
)
|
||||
|
||||
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
||||
|
Loading…
Reference in New Issue
Block a user