SaveExternalService (OAuth) on plugin load (#69764)

This commit is contained in:
Andres Martinez Gotor
2023-06-26 16:38:43 +02:00
committed by GitHub
parent f436364f9b
commit 4ff0abd0d1
16 changed files with 221 additions and 40 deletions

View File

@@ -38,6 +38,8 @@ type Cfg struct {
GrafanaComURL string
GrafanaAppURL string
Features plugins.FeatureToggles
AngularSupportEnabled bool
@@ -45,7 +47,7 @@ type Cfg struct {
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, features plugins.FeatureToggles, angularSupportEnabled bool,
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool,
grafanaComURL string) *Cfg {
return &Cfg{
log: log.New("plugin.cfg"),
@@ -62,6 +64,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti
PluginsCDNURLTemplate: pluginsCDNURLTemplate,
Tracing: tracing,
GrafanaComURL: grafanaComURL,
GrafanaAppURL: appURL,
Features: features,
AngularSupportEnabled: angularSupportEnabled,
}

View File

@@ -16,7 +16,7 @@ import (
)
type Provider interface {
Get(ctx context.Context, p *plugins.Plugin) []string
Get(ctx context.Context, p *plugins.Plugin) ([]string, error)
}
type Service struct {
@@ -31,7 +31,7 @@ func NewProvider(cfg *config.Cfg, license plugins.Licensing) *Service {
}
}
func (s *Service) Get(_ context.Context, p *plugins.Plugin) []string {
func (s *Service) Get(ctx context.Context, p *plugins.Plugin) ([]string, error) {
hostEnv := []string{
fmt.Sprintf("GF_VERSION=%s", s.cfg.BuildVersion),
}
@@ -46,13 +46,23 @@ func (s *Service) Get(_ context.Context, p *plugins.Plugin) []string {
hostEnv = append(hostEnv, s.license.Environment()...)
}
if p.ExternalService != nil {
hostEnv = append(
hostEnv,
fmt.Sprintf("GF_APP_URL=%s", s.cfg.GrafanaAppURL),
fmt.Sprintf("GF_PLUGIN_APP_CLIENT_ID=%s", p.ExternalService.ClientID),
fmt.Sprintf("GF_PLUGIN_APP_CLIENT_SECRET=%s", p.ExternalService.ClientSecret),
fmt.Sprintf("GF_PLUGIN_APP_PRIVATE_KEY=%s", p.ExternalService.PrivateKey),
)
}
hostEnv = append(hostEnv, s.awsEnvVars()...)
hostEnv = append(hostEnv, s.secureSocksProxyEnvVars()...)
hostEnv = append(hostEnv, azsettings.WriteToEnvStr(s.cfg.Azure)...)
hostEnv = append(hostEnv, s.tracingEnvVars(p)...)
ev := getPluginSettings(p.ID, s.cfg).asEnvVar("GF_PLUGIN", hostEnv)
return ev
return ev, nil
}
func (s *Service) tracingEnvVars(plugin *plugins.Plugin) []string {

View File

@@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
@@ -37,7 +39,8 @@ func TestInitializer_envVars(t *testing.T) {
},
}, licensing)
envVars := envVarsProvider.Get(context.Background(), p)
envVars, err := envVarsProvider.Get(context.Background(), p)
require.NoError(t, err)
assert.Len(t, envVars, 6)
assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[0])
assert.Equal(t, "GF_VERSION=", envVars[1])
@@ -297,8 +300,39 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
envVarsProvider := NewProvider(tc.cfg, nil)
envVars := envVarsProvider.Get(context.Background(), tc.plugin)
envVars, err := envVarsProvider.Get(context.Background(), tc.plugin)
require.NoError(t, err)
tc.exp(t, envVars)
})
}
}
func TestInitializer_oauthEnvVars(t *testing.T) {
t.Run("backend datasource with oauth registration", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
ExternalServiceRegistration: &oauth.ExternalServiceRegistration{},
},
ExternalService: &oauth.ExternalService{
ClientID: "clientID",
ClientSecret: "clientSecret",
PrivateKey: "privatePem",
},
}
envVarsProvider := NewProvider(&config.Cfg{
GrafanaAppURL: "https://myorg.com/",
Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth),
}, nil)
envVars, err := envVarsProvider.Get(context.Background(), p)
require.NoError(t, err)
assert.Len(t, envVars, 5)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_APP_URL=https://myorg.com/", envVars[1])
assert.Equal(t, "GF_PLUGIN_APP_CLIENT_ID=clientID", envVars[2])
assert.Equal(t, "GF_PLUGIN_APP_CLIENT_SECRET=clientSecret", envVars[3])
assert.Equal(t, "GF_PLUGIN_APP_PRIVATE_KEY=privatePem", envVars[4])
})
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/plugins/storage"
)
@@ -422,3 +423,11 @@ func (f *FakePluginFileStore) File(ctx context.Context, pluginID, filename strin
}
return nil, nil
}
type FakeOauthService struct {
Result *oauth.ExternalService
}
func (f *FakeOauthService) RegisterExternalService(ctx context.Context, name string, svc *oauth.ExternalServiceRegistration) (*oauth.ExternalService, error) {
return f.Result, nil
}

View File

@@ -28,7 +28,10 @@ func (i *Initializer) Initialize(ctx context.Context, p *plugins.Plugin) error {
return errors.New("could not find backend factory for plugin")
}
env := i.envVarProvider.Get(ctx, p)
env, err := i.envVarProvider.Get(ctx, p)
if err != nil {
return err
}
if backendClient, err := backendFactory(p.ID, p.Logger(), env); err != nil {
return err
} else {

View File

@@ -138,9 +138,9 @@ type fakeEnvVarsProvider struct {
GetFunc func(ctx context.Context, p *plugins.Plugin) []string
}
func (f *fakeEnvVarsProvider) Get(ctx context.Context, p *plugins.Plugin) []string {
func (f *fakeEnvVarsProvider) Get(ctx context.Context, p *plugins.Plugin) ([]string, error) {
if f.GetFunc != nil {
return f.GetFunc(ctx, p)
return f.GetFunc(ctx, p), nil
}
return nil
return nil, nil
}

View File

@@ -20,22 +20,25 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
)
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
signatureCalculator plugins.SignatureCalculator
assetPath *assetpath.Service
log log.Logger
cfg *config.Cfg
pluginFinder finder.Finder
processManager process.Service
pluginRegistry registry.Service
roleRegistry plugins.RoleRegistry
pluginInitializer initializer.Initializer
signatureValidator signature.Validator
signatureCalculator plugins.SignatureCalculator
externalServiceRegistry oauth.ExternalServiceRegistry
assetPath *assetpath.Service
log log.Logger
cfg *config.Cfg
angularInspector angularinspector.Inspector
@@ -45,29 +48,30 @@ type Loader struct {
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, signatureCalculator plugins.SignatureCalculator,
angularInspector angularinspector.Inspector) *Loader {
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader {
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector)
roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector, externalServiceRegistry)
}
func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, roleRegistry plugins.RoleRegistry,
assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator,
angularInspector angularinspector.Inspector) *Loader {
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader {
return &Loader{
pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry,
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
signatureCalculator: signatureCalculator,
processManager: processManager,
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
roleRegistry: roleRegistry,
cfg: cfg,
assetPath: assetPath,
angularInspector: angularInspector,
pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry,
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
signatureCalculator: signatureCalculator,
processManager: processManager,
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
roleRegistry: roleRegistry,
cfg: cfg,
assetPath: assetPath,
angularInspector: angularInspector,
externalServiceRegistry: externalServiceRegistry,
}
}
@@ -202,6 +206,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
}
}
if p.ExternalServiceRegistration != nil && l.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
s, err := l.externalServiceRegistry.RegisterExternalService(ctx, p.ID, p.ExternalServiceRegistration)
if err != nil {
l.log.Error("Could not register an external service. Initialization skipped", "pluginID", p.ID, "err", err)
continue
}
p.ExternalService = s
}
err := l.pluginInitializer.Initialize(ctx, p)
if err != nil {
l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err)

View File

@@ -1443,7 +1443,7 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
signature.ProvideService(cfg, statickey.New()), angularInspector)
signature.ProvideService(cfg, statickey.New()), angularInspector, &fakes.FakeOauthService{})
for _, cb := range cbs {
cb(l)

View File

@@ -123,7 +123,7 @@ func TestIntegrationPluginManager(t *testing.T) {
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New()),
angularInspector)
angularInspector, &fakes.FakeOauthService{})
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err)

View File

@@ -0,0 +1,37 @@
package oauth
import (
"context"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
// SelfCfg is a subset of oauthserver.SelfCfg making some fields optional
type SelfCfg struct {
Enabled *bool `json:"enabled,omitempty"`
Permissions []accesscontrol.Permission `json:"permissions,omitempty"`
}
// ImpersonationCfg is a subset of oauthserver.ImpersonationCfg making some fields optional
type ImpersonationCfg struct {
Enabled *bool `json:"enabled,omitempty"`
Groups *bool `json:"groups,omitempty"`
Permissions []accesscontrol.Permission `json:"permissions,omitempty"`
}
// PluginExternalServiceRegistration is a subset of oauthserver.ExternalServiceRegistration
// simplified for the plugin use case.
type ExternalServiceRegistration struct {
Impersonation *ImpersonationCfg `json:"impersonation,omitempty"`
Self *SelfCfg `json:"self,omitempty"`
}
type ExternalService struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
PrivateKey string `json:"privateKey"`
}
type ExternalServiceRegistry interface {
RegisterExternalService(ctx context.Context, name string, svc *ExternalServiceRegistration) (*ExternalService, error)
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
@@ -53,6 +54,8 @@ type Plugin struct {
AngularDetected bool
ExternalService *oauth.ExternalService
Renderer pluginextensionv2.RendererPlugin
SecretsManager secretsmanagerplugin.SecretsManagerPlugin
client backendplugin.Plugin
@@ -150,6 +153,9 @@ type JSONData struct {
// Backend (Datasource + Renderer + SecretsManager)
Executable string `json:"executable,omitempty"`
// Oauth App Service Registration
ExternalServiceRegistration *oauth.ExternalServiceRegistration `json:"externalServiceRegistration,omitempty"`
}
func ReadPluginJSON(reader io.Reader) (JSONData, error) {