Chore: Use Grafana API to retrieve the public key to validate plugins (#66439)

pull/66780/head
Andres Martinez Gotor 3 years ago committed by GitHub
parent f612a72f96
commit 98c695c68f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 4
      pkg/api/plugin_resource_test.go
  4. 9
      pkg/plugins/config/config.go
  5. 8
      pkg/plugins/ifaces.go
  6. 56
      pkg/plugins/manager/loader/loader.go
  7. 118
      pkg/plugins/manager/loader/loader_test.go
  8. 4
      pkg/plugins/manager/manager_integration_test.go
  9. 93
      pkg/plugins/manager/signature/manifest.go
  10. 23
      pkg/plugins/manager/signature/manifest_test.go
  11. 171
      pkg/plugins/manager/signature/manifestverifier/verifier.go
  12. 74
      pkg/plugins/manager/signature/manifestverifier/verifier_test.go
  13. 10
      pkg/plugins/repo/service.go
  14. 6
      pkg/services/featuremgmt/registry.go
  15. 1
      pkg/services/featuremgmt/toggles_gen.csv
  16. 4
      pkg/services/featuremgmt/toggles_gen.go
  17. 5
      pkg/services/pluginsintegration/config/config.go
  18. 2
      pkg/services/pluginsintegration/pluginsintegration.go

@ -109,6 +109,7 @@ Alpha features might be changed or removed without prior notice.
| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one | | `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 | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different |
| `authenticationConfigUI` | Enables authentication configuration UI | | `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 | | `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode |
| `opensearchDetectVersion` | Enable version detection in OpenSearch | | `opensearchDetectVersion` | Enable version detection in OpenSearch |

@ -96,6 +96,7 @@ export interface FeatureToggles {
useCachingService?: boolean; useCachingService?: boolean;
enableElasticsearchBackendQuerying?: boolean; enableElasticsearchBackendQuerying?: boolean;
authenticationConfigUI?: boolean; authenticationConfigUI?: boolean;
pluginsAPIManifestKey?: boolean;
advancedDataSourcePicker?: boolean; advancedDataSourcePicker?: boolean;
opensearchDetectVersion?: boolean; opensearchDetectVersion?: boolean;
} }

@ -60,12 +60,12 @@ func TestCallResource(t *testing.T) {
coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil, 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) 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) require.NoError(t, err)
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(), fakes.NewFakeRoleRegistry(), 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) 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)

@ -3,6 +3,7 @@ package config
import ( import (
"github.com/grafana/grafana-azure-sdk-go/azsettings" "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/plugins/log"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -34,11 +35,15 @@ type Cfg struct {
PluginsCDNURLTemplate string PluginsCDNURLTemplate string
Tracing Tracing Tracing Tracing
GrafanaComURL string
Features plugins.FeatureToggles
} }
func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings, 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{ return &Cfg{
log: log.New("plugin.cfg"), log: log.New("plugin.cfg"),
PluginsPath: pluginsPath, PluginsPath: pluginsPath,
@ -53,5 +58,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti
LogDatasourceRequests: logDatasourceRequests, LogDatasourceRequests: logDatasourceRequests,
PluginsCDNURLTemplate: pluginsCDNURLTemplate, PluginsCDNURLTemplate: pluginsCDNURLTemplate,
Tracing: tracing, 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 { func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client {
return fn(next) 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) var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct { type Loader struct {
pluginFinder finder.Finder pluginFinder finder.Finder
processManager process.Service processManager process.Service
pluginRegistry registry.Service pluginRegistry registry.Service
roleRegistry plugins.RoleRegistry roleRegistry plugins.RoleRegistry
pluginInitializer initializer.Initializer pluginInitializer initializer.Initializer
signatureValidator signature.Validator signatureValidator signature.Validator
pluginStorage storage.Manager signatureCalculator plugins.SignatureCalculator
assetPath *assetpath.Service pluginStorage storage.Manager
log log.Logger assetPath *assetpath.Service
cfg *config.Cfg log log.Logger
cfg *config.Cfg
errs map[string]*plugins.SignatureError errs map[string]*plugins.SignatureError
} }
func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, 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), return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, assetPath, storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, assetPath,
pluginFinder) pluginFinder, signatureCalculator)
} }
func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry, 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{ return &Loader{
pluginFinder: pluginFinder, pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry, pluginRegistry: pluginRegistry,
pluginInitializer: initializer.New(cfg, backendProvider, license), pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer), signatureValidator: signature.NewValidator(authorizer),
processManager: processManager, signatureCalculator: signatureCalculator,
pluginStorage: pluginStorage, processManager: processManager,
errs: make(map[string]*plugins.SignatureError), pluginStorage: pluginStorage,
log: log.New("plugin.loader"), errs: make(map[string]*plugins.SignatureError),
roleRegistry: roleRegistry, log: log.New("plugin.loader"),
cfg: cfg, roleRegistry: roleRegistry,
assetPath: assetPath, 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) { func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) {
var loadedPlugins []*plugins.Plugin var loadedPlugins []*plugins.Plugin
for _, p := range found { for _, p := range found {
if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists { 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) l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID)
continue continue
} }
sig, err := signature.Calculate(ctx, l.log, src, p.Primary) sig, err := l.signatureCalculator.Calculate(ctx, src, p.Primary)
if err != nil { if err != nil {
l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err)
continue continue

@ -2,7 +2,10 @@ package loader
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -21,8 +24,10 @@ 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/manifestverifier"
"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/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "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) { func TestLoader_Load_NestedPlugins(t *testing.T) {
rootDir, err := filepath.Abs("../") rootDir, err := filepath.Abs("../")
if err != nil { if err != nil {
@ -1435,7 +1551,7 @@ func Test_setPathsBasedOnApp(t *testing.T) {
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { 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.NewFakePluginStorage(), 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 { for _, cb := range cbs {
cb(l) 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) 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) require.NoError(t, err)
reg := registry.ProvideService() reg := registry.ProvideService()
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(), fakes.NewFakeRoleRegistry(), 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) 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,7 +1,6 @@
package signature package signature
import ( import (
"bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@ -16,42 +15,15 @@ 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/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"
) )
// 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 ( var (
runningWindows = runtime.GOOS == "windows" runningWindows = runtime.GOOS == "windows"
@ -79,9 +51,24 @@ func (m *PluginManifest) isV2() bool {
return strings.HasPrefix(m.ManifestVersion, "2.") 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 // 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) block, _ := clearsign.Decode(body)
if block == nil { if block == nil {
return nil, errors.New("unable to decode manifest") 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) 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 nil, err
} }
return &manifest, nil 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 { if defaultSignature, exists := src.DefaultSignature(ctx); exists {
return defaultSignature, nil return defaultSignature, nil
} }
if len(plugin.FS.Files()) == 0 { 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, nil
@ -116,13 +103,13 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
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) {
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{ return plugins.Signature{
Status: plugins.SignatureUnsigned, Status: plugins.SignatureUnsigned,
}, nil }, 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, nil
@ -132,21 +119,21 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
return return
} }
if err = f.Close(); err != nil { 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) byteValue, err := io.ReadAll(f)
if err != nil || len(byteValue) < 10 { 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{ return plugins.Signature{
Status: plugins.SignatureUnsigned, Status: plugins.SignatureUnsigned,
}, nil }, nil
} }
manifest, err := ReadPluginManifest(byteValue) manifest, err := s.readPluginManifest(byteValue)
if err != nil { 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, 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 // 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 {
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 return plugins.Signature{}, err
} else if !match { } 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) "appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs)
return plugins.Signature{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
@ -183,7 +170,7 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
// Verify the manifest contents // Verify the manifest contents
for p, hash := range manifest.Files { for p, hash := range manifest.Files {
err = verifyHash(mlog, plugin, p, hash) err = verifyHash(s.mlog, plugin, p, hash)
if err != nil { if err != nil {
return plugins.Signature{ return plugins.Signature{
Status: plugins.SignatureModified, Status: plugins.SignatureModified,
@ -213,13 +200,13 @@ func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, p
} }
if len(unsignedFiles) > 0 { 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{ return plugins.Signature{
Status: plugins.SignatureModified, Status: plugins.SignatureModified,
}, nil }, nil
} }
mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID) s.mlog.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,
@ -299,7 +286,7 @@ func (r invalidFieldErr) Error() string {
return fmt.Sprintf("valid manifest field %s is required", r.field) 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 { if len(m.Plugin) == 0 {
return invalidFieldErr{field: "plugin"} 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) 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, return s.verifier.Verify(m.KeyID, block)
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body, &packet.Config{}); err != nil {
return fmt.Errorf("%v: %w", "failed to check signature", err)
}
return nil
} }

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins" "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/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -49,7 +49,8 @@ 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) {
manifest, err := ReadPluginManifest([]byte(txt)) s := ProvideService(&config.Cfg{})
manifest, err := s.readPluginManifest([]byte(txt))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, manifest) require.NotNil(t, manifest)
@ -65,7 +66,8 @@ 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")
_, err := ReadPluginManifest([]byte(modified)) s := ProvideService(&config.Cfg{})
_, err := s.readPluginManifest([]byte(modified))
require.Error(t, err) require.Error(t, err)
}) })
} }
@ -102,7 +104,8 @@ 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) {
manifest, err := ReadPluginManifest([]byte(txt)) s := ProvideService(&config.Cfg{})
manifest, err := s.readPluginManifest([]byte(txt))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, manifest) require.NotNil(t, manifest)
@ -155,7 +158,8 @@ 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")
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 { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External return plugins.External
}, },
@ -185,7 +189,8 @@ func TestCalculate(t *testing.T) {
basePath := "../testdata/renderer-added-file/plugin" basePath := "../testdata/renderer-added-file/plugin"
runningWindows = true 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 { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External return plugins.External
}, },
@ -233,7 +238,8 @@ func TestCalculate(t *testing.T) {
basePath := "../testdata/app-with-child/dist" 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 { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External return plugins.External
}, },
@ -677,7 +683,8 @@ 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) {
err := validateManifest(*tc.manifest, nil) s := ProvideService(&config.Cfg{})
err := s.validateManifest(*tc.manifest, nil)
require.Errorf(t, err, tc.expectedErr) require.Errorf(t, err, tc.expectedErr)
}) })
} }

@ -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" "path"
"strings" "strings"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
) )
@ -18,9 +19,12 @@ type Manager struct {
log log.PrettyLogger log log.PrettyLogger
} }
func ProvideService() *Manager { func ProvideService(cfg *config.Cfg) (*Manager, error) {
defaultBaseURL := "https://grafana.com/api/plugins" defaultBaseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins")
return New(false, defaultBaseURL, log.NewPrettyLogger("plugin.repository")) 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 { func New(skipTLSVerify bool, baseURL string, logger log.PrettyLogger) *Manager {

@ -518,6 +518,12 @@ var (
State: FeatureStateAlpha, State: FeatureStateAlpha,
Owner: grafanaAuthnzSquad, Owner: grafanaAuthnzSquad,
}, },
{
Name: "pluginsAPIManifestKey",
Description: "Use grafana.com API to retrieve the public manifest key",
State: FeatureStateAlpha,
Owner: grafanaPluginsPlatformSquad,
},
{ {
Name: "advancedDataSourcePicker", Name: "advancedDataSourcePicker",
Description: "Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode", 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 useCachingService,stable,@grafana/grafana-operator-experience-squad,false,false,true,false
enableElasticsearchBackendQuerying,beta,@grafana/observability-logs,false,false,false,false enableElasticsearchBackendQuerying,beta,@grafana/observability-logs,false,false,false,false
authenticationConfigUI,alpha,@grafana/grafana-authnz-team,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 advancedDataSourcePicker,alpha,@grafana/dashboards-squad,false,false,false,true
opensearchDetectVersion,alpha,@grafana/aws-plugins,false,false,false,true opensearchDetectVersion,alpha,@grafana/aws-plugins,false,false,false,true

1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
77 useCachingService stable @grafana/grafana-operator-experience-squad false false true false
78 enableElasticsearchBackendQuerying beta @grafana/observability-logs false false false false
79 authenticationConfigUI alpha @grafana/grafana-authnz-team false false false false
80 pluginsAPIManifestKey alpha @grafana/plugins-platform-backend false false false false
81 advancedDataSourcePicker alpha @grafana/dashboards-squad false false false true
82 opensearchDetectVersion alpha @grafana/aws-plugins false false false true

@ -319,6 +319,10 @@ const (
// Enables authentication configuration UI // Enables authentication configuration UI
FlagAuthenticationConfigUI = "authenticationConfigUI" FlagAuthenticationConfigUI = "authenticationConfigUI"
// FlagPluginsAPIManifestKey
// Use grafana.com API to retrieve the public manifest key
FlagPluginsAPIManifestKey = "pluginsAPIManifestKey"
// FlagAdvancedDataSourcePicker // FlagAdvancedDataSourcePicker
// Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode // Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode
FlagAdvancedDataSourcePicker = "advancedDataSourcePicker" FlagAdvancedDataSourcePicker = "advancedDataSourcePicker"

@ -5,10 +5,11 @@ import (
"strings" "strings"
pCfg "github.com/grafana/grafana/pkg/plugins/config" pCfg "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "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") plugins := settingProvider.Section("plugins")
allowedUnsigned := grafanaCfg.PluginsAllowUnsigned allowedUnsigned := grafanaCfg.PluginsAllowUnsigned
if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 { 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 { if err != nil {
return nil, fmt.Errorf("new opentelemetry cfg: %w", err) return nil, fmt.Errorf("new opentelemetry cfg: %w", err)
} }
return pCfg.NewCfg( return pCfg.NewCfg(
settingProvider.KeyValue("", "app_mode").MustBool(grafanaCfg.Env == setting.Dev), settingProvider.KeyValue("", "app_mode").MustBool(grafanaCfg.Env == setting.Dev),
grafanaCfg.PluginsPath, grafanaCfg.PluginsPath,
@ -38,6 +40,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) (*
grafanaCfg.PluginLogBackendRequests, grafanaCfg.PluginLogBackendRequests,
grafanaCfg.PluginsCDNURLTemplate, grafanaCfg.PluginsCDNURLTemplate,
tracingCfg, tracingCfg,
featuremgmt.ProvideToggles(features),
), nil ), nil
} }

@ -66,6 +66,8 @@ var WireSet = wire.NewSet(
wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)),
filestore.ProvideService, filestore.ProvideService,
wire.Bind(new(plugins.FileStore), new(*filestore.Service)), 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 // WireExtensionSet provides a wire.ProviderSet of plugin providers that can be

Loading…
Cancel
Save