IDforwarding: forward signed id to plugins (#75651)

* Plugins: Add client middlware that forwards the signed grafana id token if present

* DsProxy: Set grafana id header if id token exists

* Add util function to apply id token to header

* Only add id forwarding middleware if feature toggle is enabled

* Add feature toggles to ds proxy and check if id forwarding is enabled

* Clean up test setup

* Change to use backend.ForwardHTTPHeaders interface

* PluginProxy: Forward signed identity when feature toggle is enabled

* PluginProxy: forrward signed id header
This commit is contained in:
Karl Persson 2023-10-02 09:14:10 +02:00 committed by GitHub
parent 5892353bbd
commit 684d68365e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 280 additions and 257 deletions

View File

@ -51,7 +51,7 @@ func (hs *HTTPServer) ProxyPluginRequest(c *contextmodel.ReqContext) {
}
proxyPath := getProxyPath(c)
p, err := pluginproxy.NewPluginProxy(ps, plugin.Routes, c, proxyPath, hs.Cfg, hs.SecretsService, hs.tracer, pluginProxyTransport)
p, err := pluginproxy.NewPluginProxy(ps, plugin.Routes, c, proxyPath, hs.Cfg, hs.SecretsService, hs.tracer, pluginProxyTransport, hs.Features)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create plugin proxy", err)
return

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -43,6 +44,7 @@ type DataSourceProxy struct {
oAuthTokenService oauthtoken.OAuthTokenService
dataSourcesService datasources.DataSourceService
tracer tracing.Tracer
features featuremgmt.FeatureToggles
}
type httpClient interface {
@ -53,7 +55,7 @@ type httpClient interface {
func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext,
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService,
tracer tracing.Tracer) (*DataSourceProxy, error) {
tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*DataSourceProxy, error) {
targetURL, err := datasource.ValidateURL(ds.Type, ds.URL)
if err != nil {
return nil, err
@ -70,6 +72,7 @@ func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Rout
oAuthTokenService: oAuthTokenService,
dataSourcesService: dsService,
tracer: tracer,
features: features,
}, nil
}
@ -262,6 +265,10 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
}
}
}
if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) {
proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser)
}
}
func (proxy *DataSourceProxy) validateRequest() error {

View File

@ -22,18 +22,20 @@ import (
"github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets"
@ -47,8 +49,6 @@ import (
func TestDataSourceProxy_routeRule(t *testing.T) {
cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
tracer := tracing.InitializeTracerForTest()
t.Run("Plugin with routes", func(t *testing.T) {
routes := []*plugins.Route{
@ -96,27 +96,12 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
origSecretKey := setting.SecretKey
t.Cleanup(func() {
setting.SecretKey = origSecretKey
})
setting.SecretKey = "password" //nolint:goconst
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
key, err := secretsService.Encrypt(context.Background(), []byte("123"), secrets.WithoutScope())
require.NoError(t, err)
ds := &datasources.DataSource{
JsonData: simplejson.NewFromAny(map[string]any{
"clientId": "asd",
"dynamicUrl": "https://dynamic.grafana.com",
"queryParam": "apiKey",
}),
SecureJsonData: map[string][]byte{
"key": key,
},
}
jd, err := ds.JsonData.Map()
@ -142,14 +127,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
&oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/v4/some/method")
require.NoError(t, err)
proxy.matchedRoute = routes[0]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg)
assert.Equal(t, "https://www.google.com/some/method", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
@ -157,13 +138,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/common/some/method")
require.NoError(t, err)
proxy.matchedRoute = routes[3]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg)
assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
@ -171,26 +149,20 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
require.NoError(t, err)
proxy.matchedRoute = routes[4]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg)
assert.Equal(t, "http://localhost/asd", req.URL.String())
})
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/body")
require.NoError(t, err)
proxy.matchedRoute = routes[5]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg)
content, err := io.ReadAll(req.Body)
require.NoError(t, err)
@ -200,10 +172,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/v4/some/method")
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@ -211,10 +180,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/admin")
require.NoError(t, err)
err = proxy.validateRequest()
require.Error(t, err)
@ -223,10 +189,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp()
ctx.SignedInUser.OrgRole = org.RoleAdmin
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/admin")
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@ -264,26 +227,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
origSecretKey := setting.SecretKey
t.Cleanup(func() {
setting.SecretKey = origSecretKey
})
setting.SecretKey = "password"
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
key, err := secretsService.Encrypt(context.Background(), []byte("123"), secrets.WithoutScope())
require.NoError(t, err)
ds := &datasources.DataSource{
JsonData: simplejson.NewFromAny(map[string]any{
"clientId": "asd",
"tenantId": "mytenantId",
}),
SecureJsonData: map[string][]byte{
"clientSecret": key,
},
}
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
@ -316,12 +264,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken1")
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg)
authorizationHeaderCall1 = req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())
@ -334,12 +279,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err)
client = newFakeHTTPClient(t, json2)
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken2")
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, proxy.cfg)
authorizationHeaderCall2 = req.Header.Get("Authorization")
@ -353,12 +297,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken1")
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg)
authorizationHeaderCall3 := req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())
@ -376,13 +318,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ds := &datasources.DataSource{URL: "htttp://graphite:8080", Type: datasources.DS_GRAPHITE}
ctx := &contextmodel.ReqContext{}
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(proxy *DataSourceProxy) {
proxy.cfg = &setting.Cfg{BuildVersion: "5.3.0"}
})
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@ -405,13 +343,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -433,13 +365,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub")
@ -464,14 +390,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
}
ctx := &contextmodel.ReqContext{}
var pluginRoutes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
var routes []*plugins.Route
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub")
@ -492,14 +412,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
}
ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/")
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Set("Origin", "grafana.com")
req.Header.Set("Referer", "grafana.com")
@ -529,30 +445,24 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
Context: &web.Context{Req: req},
}
token := &oauth2.Token{
AccessToken: "testtoken",
RefreshToken: "testrefreshtoken",
TokenType: "Bearer",
Expiry: time.Now().AddDate(0, 0, 1),
}
extra := map[string]any{
"id_token": "testidtoken",
}
token = token.WithExtra(extra)
mockAuthToken := mockOAuthTokenService{
token: token,
oAuthEnabled: true,
}
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/", func(proxy *DataSourceProxy) {
proxy.oAuthTokenService = &oauthtokentest.MockOauthTokenService{
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
return (&oauth2.Token{
AccessToken: "testtoken",
RefreshToken: "testrefreshtoken",
TokenType: "Bearer",
Expiry: time.Now().AddDate(0, 0, 1),
}).WithExtra(map[string]any{"id_token": "testidtoken"})
},
IsOAuthPassThruEnabledFunc: func(ds *datasources.DataSource) bool {
return true
},
}
})
require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@ -628,8 +538,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
// test DataSourceProxy request handling.
func TestDataSourceProxy_requestHandling(t *testing.T) {
cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
var writeErr error
type setUpCfg struct {
@ -679,18 +587,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
}, ds
}
tracer := tracing.InitializeTracerForTest()
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
require.NoError(t, err)
proxy.HandleRequest()
@ -706,13 +606,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
},
})
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
require.NoError(t, err)
proxy.HandleRequest()
@ -724,13 +618,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
require.NoError(t, err)
proxy.HandleRequest()
@ -750,13 +638,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
},
})
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
require.NoError(t, err)
proxy.HandleRequest()
@ -779,13 +661,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/%2Ftest%2Ftest%2F")
require.NoError(t, err)
proxy.HandleRequest()
@ -794,6 +670,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
require.NotNil(t, req)
require.Equal(t, "/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", req.RequestURI)
})
t.Run("Data source should handle proxy path url encoding correctly with opentelemetry", func(t *testing.T) {
var req *http.Request
ctx, ds := setUp(t, setUpCfg{
@ -806,14 +683,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
})
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/%2Ftest%2Ftest%2F")
require.NoError(t, err)
proxy.HandleRequest()
@ -832,17 +704,9 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
Type: "test",
URL: "://host/root",
}
cfg := &setting.Cfg{}
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
var err error
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
_, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/mehtod")
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
}
@ -856,17 +720,9 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
Type: "test",
URL: "127.0.01:5432",
}
cfg := &setting.Cfg{}
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
_, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/mehtod")
require.NoError(t, err)
}
@ -877,7 +733,6 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
Context: &web.Context{},
SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor},
}
tracer := tracing.InitializeTracerForTest()
tcs := []struct {
description string
@ -899,20 +754,13 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
}
for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
cfg := &setting.Cfg{}
ds := datasources.DataSource{
Type: "mssql",
URL: tc.url,
}
var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
p, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/method")
if tc.err == nil {
require.NoError(t, err)
assert.Equal(t, &url.URL{
@ -939,10 +787,11 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(cfg), &actest.FakePermissionsService{}, quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err)
@ -1058,10 +907,11 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(cfg), &actest.FakePermissionsService{}, quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -1073,7 +923,6 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
}
func Test_PathCheck(t *testing.T) {
cfg := &setting.Cfg{}
// Ensure that we test routes appropriately. This test reproduces a historical bug where two routes were defined with different role requirements but the same method and the more privileged route was tested first. Here we ensure auth checks are applied based on the correct route, not just the method.
routes := []*plugins.Route{
{
@ -1089,7 +938,6 @@ func Test_PathCheck(t *testing.T) {
Method: http.MethodGet,
},
}
tracer := tracing.InitializeTracerForTest()
setUp := func() (*contextmodel.ReqContext, *http.Request) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
@ -1101,40 +949,33 @@ func Test_PathCheck(t *testing.T) {
return ctx, req
}
ctx, _ := setUp()
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(&datasources.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
proxy, err := setupDSProxyTest(t, ctx, &datasources.DataSource{}, routes, "b")
require.NoError(t, err)
require.Nil(t, proxy.validateRequest())
require.Equal(t, routes[1], proxy.matchedRoute)
}
type mockOAuthTokenService struct {
token *oauth2.Token
oAuthEnabled bool
}
func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasources.DataSource, routes []*plugins.Route, path string, opts ...func(proxy *DataSourceProxy)) (*DataSourceProxy, error) {
t.Helper()
func (m *mockOAuthTokenService) GetCurrentOAuthToken(ctx context.Context, user identity.Requester) *oauth2.Token {
return m.token
}
cfg := setting.NewCfg()
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
features := featuremgmt.WithFeatures()
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(cfg), &actest.FakePermissionsService{}, quotatest.New(false, nil))
require.NoError(t, err)
func (m *mockOAuthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
return m.oAuthEnabled
}
tracer := tracing.InitializeTracerForTest()
func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) {
return nil, false, nil
}
proxy, err := NewDataSourceProxy(ds, routes, ctx, path, cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
if err != nil {
return nil, err
}
func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *login.UserAuth) error {
return nil
}
for _, o := range opts {
o(proxy)
}
func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *login.UserAuth) error {
return nil
return proxy, nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
@ -29,12 +30,13 @@ type PluginProxy struct {
secretsService secrets.Service
tracer tracing.Tracer
transport *http.Transport
features featuremgmt.FeatureToggles
}
// NewPluginProxy creates a plugin proxy.
func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *contextmodel.ReqContext,
proxyPath string, cfg *setting.Cfg, secretsService secrets.Service, tracer tracing.Tracer,
transport *http.Transport) (*PluginProxy, error) {
transport *http.Transport, features featuremgmt.FeatureToggles) (*PluginProxy, error) {
return &PluginProxy{
ps: ps,
pluginRoutes: routes,
@ -44,6 +46,7 @@ func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *contex
secretsService: secretsService,
tracer: tracer,
transport: transport,
features: features,
}, nil
}
@ -156,6 +159,10 @@ func (proxy PluginProxy) director(req *http.Request) {
proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
if proxy.features.IsEnabled(featuremgmt.FlagIdForwarding) {
proxyutil.ApplyForwardIDHeader(req, proxy.ctx.SignedInUser)
}
if err := addHeaders(&req.Header, proxy.matchedRoute, data); err != nil {
proxy.ctx.JsonApiErr(500, "Failed to render plugin headers", err)
return

View File

@ -9,9 +9,13 @@ import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets"
@ -20,8 +24,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPluginProxy(t *testing.T) {
@ -260,7 +262,7 @@ func TestPluginProxy(t *testing.T) {
ps := &pluginsettings.DTO{
SecureJSONData: map[string][]byte{},
}
proxy, err := NewPluginProxy(ps, routes, ctx, "", &setting.Cfg{}, secretsService, tracing.InitializeTracerForTest(), &http.Transport{})
proxy, err := NewPluginProxy(ps, routes, ctx, "", &setting.Cfg{}, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, featuremgmt.WithFeatures())
require.NoError(t, err)
proxy.HandleRequest()
@ -398,7 +400,7 @@ func TestPluginProxyRoutes(t *testing.T) {
ps := &pluginsettings.DTO{
SecureJSONData: map[string][]byte{},
}
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, &setting.Cfg{}, secretsService, tracing.InitializeTracerForTest(), &http.Transport{})
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, &setting.Cfg{}, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, featuremgmt.WithFeatures())
require.NoError(t, err)
proxy.HandleRequest()
@ -429,7 +431,7 @@ func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsServic
ReqRole: org.RoleEditor,
}
}
proxy, err := NewPluginProxy(ps, []*plugins.Route{}, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{})
proxy, err := NewPluginProxy(ps, []*plugins.Route{}, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, featuremgmt.WithFeatures())
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "/api/plugin-proxy/grafana-simple-app/api/v4/alerts", nil)

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/secrets"
@ -26,7 +27,7 @@ import (
func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator validations.PluginRequestValidator,
pluginStore pluginstore.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider,
oauthTokenService *oauthtoken.Service, dsService datasources.DataSourceService,
tracer tracing.Tracer, secretsService secrets.Service) *DataSourceProxyService {
tracer tracing.Tracer, secretsService secrets.Service, features featuremgmt.FeatureToggles) *DataSourceProxyService {
return &DataSourceProxyService{
DataSourceCache: dataSourceCache,
PluginRequestValidator: plugReqValidator,
@ -50,6 +51,7 @@ type DataSourceProxyService struct {
DataSourcesService datasources.DataSourceService
tracer tracing.Tracer
secretsService secrets.Service
fetures featuremgmt.FeatureToggles
}
func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) {
@ -120,7 +122,7 @@ func (p *DataSourceProxyService) proxyDatasourceRequest(c *contextmodel.ReqConte
proxyPath := getProxyPath(c)
proxy, err := pluginproxy.NewDataSourceProxy(ds, plugin.Routes, c, proxyPath, p.Cfg, p.HTTPClientProvider,
p.OAuthTokenService, p.DataSourcesService, p.tracer)
p.OAuthTokenService, p.DataSourcesService, p.tracer, p.fetures)
if err != nil {
var urlValidationError datasource.URLValidationError
if errors.As(err, &urlValidationError) {

View File

@ -0,0 +1,96 @@
package clientmiddleware
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/contexthandler"
)
const forwardIDHeaderName = "X-Grafana-Id"
// NewForwardIDMiddleware creates a new plugins.ClientMiddleware that will
// set grafana id header on outgoing plugins.Client requests if the
// feature toggle FlagIdForwarding is enabled
func NewForwardIDMiddleware() plugins.ClientMiddleware {
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client {
return &ForwardIDMiddleware{
next: next,
}
})
}
type ForwardIDMiddleware struct {
next plugins.Client
}
func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.PluginContext, req backend.ForwardHTTPHeaders) error {
reqCtx := contexthandler.FromContext(ctx)
// if request not for a datasource or no HTTP request context skip middleware
if req == nil || reqCtx == nil || reqCtx.SignedInUser == nil {
return nil
}
// token will only be present if faeturemgmt.FlagIdForwarding is enabled
if token := reqCtx.SignedInUser.GetIDToken(); token != "" {
req.SetHTTPHeader(forwardIDHeaderName, token)
}
return nil
}
func (m *ForwardIDMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if req == nil {
return m.next.QueryData(ctx, req)
}
err := m.applyToken(ctx, req.PluginContext, req)
if err != nil {
return nil, err
}
return m.next.QueryData(ctx, req)
}
func (m *ForwardIDMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
if req == nil {
return m.next.CallResource(ctx, req, sender)
}
err := m.applyToken(ctx, req.PluginContext, req)
if err != nil {
return err
}
return m.next.CallResource(ctx, req, sender)
}
func (m *ForwardIDMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
if req == nil {
return m.next.CheckHealth(ctx, req)
}
err := m.applyToken(ctx, req.PluginContext, req)
if err != nil {
return nil, err
}
return m.next.CheckHealth(ctx, req)
}
func (m *ForwardIDMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
return m.next.CollectMetrics(ctx, req)
}
func (m *ForwardIDMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
return m.next.SubscribeStream(ctx, req)
}
func (m *ForwardIDMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
return m.next.PublishStream(ctx, req)
}
func (m *ForwardIDMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
return m.next.RunStream(ctx, req, sender)
}

View File

@ -0,0 +1,50 @@
package clientmiddleware
import (
"context"
"net/http"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/manager/client/clienttest"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/user"
)
func TestForwardIDMiddleware(t *testing.T) {
t.Run("Should set forwarded id header if present", func(t *testing.T) {
cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware()))
ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{
Context: &web.Context{Req: &http.Request{}},
SignedInUser: &user.SignedInUser{IDToken: "some-token"},
})
err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{
PluginContext: backend.PluginContext{},
}, nopCallResourceSender)
require.NoError(t, err)
require.Equal(t, "some-token", cdt.CallResourceReq.Headers[forwardIDHeaderName][0])
})
t.Run("Should not set forwarded id header if not present", func(t *testing.T) {
cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware()))
ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{
Context: &web.Context{Req: &http.Request{}},
SignedInUser: &user.SignedInUser{},
})
err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{
PluginContext: backend.PluginContext{},
}, nopCallResourceSender)
require.NoError(t, err)
require.Len(t, cdt.CallResourceReq.Headers[forwardIDHeaderName], 0)
})
}

View File

@ -167,6 +167,10 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken
middlewares = append(middlewares, clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features))
}
if features.IsEnabled(featuremgmt.FlagIdForwarding) {
middlewares = append(middlewares, clientmiddleware.NewForwardIDMiddleware())
}
if cfg.SendUserHeader {
middlewares = append(middlewares, clientmiddleware.NewUserHeaderMiddleware())
}

View File

@ -10,8 +10,12 @@ import (
"github.com/grafana/grafana/pkg/services/auth/identity"
)
// UserHeaderName name of the header used when forwarding the Grafana user login.
const UserHeaderName = "X-Grafana-User"
const (
// UserHeaderName name of the header used when forwarding the Grafana user login.
UserHeaderName = "X-Grafana-User"
// IDHeaderName name of the header used when forwarding singed id token of the user
IDHeaderName = "X-Grafana-Id"
)
// PrepareProxyRequest prepares a request for being proxied.
// Removes X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, Origin, Referer headers.
@ -116,3 +120,13 @@ func ApplyUserHeader(sendUserHeader bool, req *http.Request, user identity.Reque
req.Header.Set(UserHeaderName, user.GetLogin())
}
}
func ApplyForwardIDHeader(req *http.Request, user identity.Requester) {
if user == nil || user.IsNil() {
return
}
if token := user.GetIDToken(); token != "" {
req.Header.Set(IDHeaderName, token)
}
}