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
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) 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 { if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create plugin proxy", err) c.JsonApiErr(http.StatusInternalServerError, "Failed to create plugin proxy", err)
return return

View File

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

View File

@ -22,18 +22,20 @@ import (
"github.com/grafana/grafana/pkg/api/datasource" "github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db" "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/httpclient"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
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" "github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt" "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"
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
@ -47,8 +49,6 @@ import (
func TestDataSourceProxy_routeRule(t *testing.T) { func TestDataSourceProxy_routeRule(t *testing.T) {
cfg := &setting.Cfg{} cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
tracer := tracing.InitializeTracerForTest()
t.Run("Plugin with routes", func(t *testing.T) { t.Run("Plugin with routes", func(t *testing.T) {
routes := []*plugins.Route{ 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{ ds := &datasources.DataSource{
JsonData: simplejson.NewFromAny(map[string]any{ JsonData: simplejson.NewFromAny(map[string]any{
"clientId": "asd", "clientId": "asd",
"dynamicUrl": "https://dynamic.grafana.com", "dynamicUrl": "https://dynamic.grafana.com",
"queryParam": "apiKey", "queryParam": "apiKey",
}), }),
SecureJsonData: map[string][]byte{
"key": key,
},
} }
jd, err := ds.JsonData.Map() 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) { t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp() ctx, req := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/v4/some/method")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.matchedRoute = routes[0] 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, "https://www.google.com/some/method", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header")) 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) { t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp() ctx, req := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/common/some/method")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.matchedRoute = routes[3] 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, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header")) 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) { t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp() ctx, req := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.matchedRoute = routes[4] 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()) assert.Equal(t, "http://localhost/asd", req.URL.String())
}) })
t.Run("When matching route path and has dynamic body", func(t *testing.T) { t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp() ctx, req := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/body")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.matchedRoute = routes[5] 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) content, err := io.ReadAll(req.Body)
require.NoError(t, err) require.NoError(t, err)
@ -200,10 +172,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) { t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) { t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp() ctx, _ := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/v4/some/method")
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)
require.NoError(t, err) require.NoError(t, err)
err = proxy.validateRequest() err = proxy.validateRequest()
require.NoError(t, err) 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) { t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp() ctx, _ := setUp()
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/admin")
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)
require.NoError(t, err) require.NoError(t, err)
err = proxy.validateRequest() err = proxy.validateRequest()
require.Error(t, err) 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) { t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp() ctx, _ := setUp()
ctx.SignedInUser.OrgRole = org.RoleAdmin ctx.SignedInUser.OrgRole = org.RoleAdmin
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/admin")
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)
require.NoError(t, err) require.NoError(t, err)
err = proxy.validateRequest() err = proxy.validateRequest()
require.NoError(t, err) 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{ ds := &datasources.DataSource{
JsonData: simplejson.NewFromAny(map[string]any{ JsonData: simplejson.NewFromAny(map[string]any{
"clientId": "asd", "clientId": "asd",
"tenantId": "mytenantId", "tenantId": "mytenantId",
}), }),
SecureJsonData: map[string][]byte{
"clientSecret": key,
},
} }
req, err := http.NewRequest("GET", "http://localhost/asd", nil) req, err := http.NewRequest("GET", "http://localhost/asd", nil)
@ -316,12 +264,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
}, },
} }
quotaService := quotatest.New(false, nil) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken1")
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err) require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
authorizationHeaderCall1 = req.Header.Get("Authorization") authorizationHeaderCall1 = req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) 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) req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err) require.NoError(t, err)
client = newFakeHTTPClient(t, json2) 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) 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, proxy.cfg)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
authorizationHeaderCall2 = req.Header.Get("Authorization") authorizationHeaderCall2 = req.Header.Get("Authorization")
@ -353,12 +297,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{}) 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) require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
authorizationHeaderCall3 := req.Header.Get("Authorization") authorizationHeaderCall3 := req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) 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} ds := &datasources.DataSource{URL: "htttp://graphite:8080", Type: datasources.DS_GRAPHITE}
ctx := &contextmodel.ReqContext{} ctx := &contextmodel.ReqContext{}
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(proxy *DataSourceProxy) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) proxy.cfg = &setting.Cfg{BuildVersion: "5.3.0"}
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)
require.NoError(t, err) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err) require.NoError(t, err)
@ -405,13 +343,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &contextmodel.ReqContext{} ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
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)
require.NoError(t, err) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -433,13 +365,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
ctx := &contextmodel.ReqContext{} ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
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)
require.NoError(t, err) require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub") requestURL, err := url.Parse("http://grafana.com/sub")
@ -464,14 +390,8 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
} }
ctx := &contextmodel.ReqContext{} ctx := &contextmodel.ReqContext{}
var pluginRoutes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "")
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)
require.NoError(t, err) require.NoError(t, err)
requestURL, err := url.Parse("http://grafana.com/sub") requestURL, err := url.Parse("http://grafana.com/sub")
@ -492,14 +412,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
} }
ctx := &contextmodel.ReqContext{} ctx := &contextmodel.ReqContext{}
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/")
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)
require.NoError(t, err) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Set("Origin", "grafana.com") req.Header.Set("Origin", "grafana.com")
req.Header.Set("Referer", "grafana.com") req.Header.Set("Referer", "grafana.com")
@ -529,30 +445,24 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
Context: &web.Context{Req: req}, 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 var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/", func(proxy *DataSourceProxy) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) proxy.oAuthTokenService = &oauthtokentest.MockOauthTokenService{
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
quotaService := quotatest.New(false, nil) return (&oauth2.Token{
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService) AccessToken: "testtoken",
require.NoError(t, err) RefreshToken: "testrefreshtoken",
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer) 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) require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err) require.NoError(t, err)
@ -628,8 +538,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
// test DataSourceProxy request handling. // test DataSourceProxy request handling.
func TestDataSourceProxy_requestHandling(t *testing.T) { func TestDataSourceProxy_requestHandling(t *testing.T) {
cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
var writeErr error var writeErr error
type setUpCfg struct { type setUpCfg struct {
@ -679,18 +587,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
}, ds }, ds
} }
tracer := tracing.InitializeTracerForTest()
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) { t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
ctx, ds := setUp(t) ctx, ds := setUp(t)
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -706,13 +606,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
}, },
}) })
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() 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) { t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
ctx, ds := setUp(t) ctx, ds := setUp(t)
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -750,13 +638,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
}, },
}) })
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() 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) ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/%2Ftest%2Ftest%2F")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -794,6 +670,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
require.NotNil(t, req) require.NotNil(t, req)
require.Equal(t, "/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", req.RequestURI) 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) { t.Run("Data source should handle proxy path url encoding correctly with opentelemetry", func(t *testing.T) {
var req *http.Request var req *http.Request
ctx, ds := setUp(t, setUpCfg{ 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) ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/%2Ftest%2Ftest%2F")
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)
require.NoError(t, err) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -832,17 +704,9 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
Type: "test", Type: "test",
URL: "://host/root", URL: "://host/root",
} }
cfg := &setting.Cfg{}
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) _, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/mehtod")
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)
require.Error(t, err) require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`)) 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", Type: "test",
URL: "127.0.01:5432", URL: "127.0.01:5432",
} }
cfg := &setting.Cfg{}
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) _, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/mehtod")
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)
require.NoError(t, err) require.NoError(t, err)
} }
@ -877,7 +733,6 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
Context: &web.Context{}, Context: &web.Context{},
SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor},
} }
tracer := tracing.InitializeTracerForTest()
tcs := []struct { tcs := []struct {
description string description string
@ -899,20 +754,13 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
} }
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cfg := &setting.Cfg{}
ds := datasources.DataSource{ ds := datasources.DataSource{
Type: "mssql", Type: "mssql",
URL: tc.url, URL: tc.url,
} }
var routes []*plugins.Route var routes []*plugins.Route
sqlStore := db.InitTestDB(t) p, err := setupDSProxyTest(t, &ctx, &ds, routes, "api/method")
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)
if tc.err == nil { if tc.err == nil {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &url.URL{ assert.Equal(t, &url.URL{
@ -939,10 +787,11 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
sqlStore := db.InitTestDB(t) sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil) 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) 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) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
require.NoError(t, err) require.NoError(t, err)
@ -1058,10 +907,11 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
tracer := tracing.InitializeTracerForTest() tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route var routes []*plugins.Route
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil) 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) 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) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) 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) { 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. // 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{ routes := []*plugins.Route{
{ {
@ -1089,7 +938,6 @@ func Test_PathCheck(t *testing.T) {
Method: http.MethodGet, Method: http.MethodGet,
}, },
} }
tracer := tracing.InitializeTracerForTest()
setUp := func() (*contextmodel.ReqContext, *http.Request) { setUp := func() (*contextmodel.ReqContext, *http.Request) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil) req, err := http.NewRequest("GET", "http://localhost/asd", nil)
@ -1101,40 +949,33 @@ func Test_PathCheck(t *testing.T) {
return ctx, req return ctx, req
} }
ctx, _ := setUp() ctx, _ := setUp()
sqlStore := db.InitTestDB(t) proxy, err := setupDSProxyTest(t, ctx, &datasources.DataSource{}, routes, "b")
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)
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, proxy.validateRequest()) require.Nil(t, proxy.validateRequest())
require.Equal(t, routes[1], proxy.matchedRoute) require.Equal(t, routes[1], proxy.matchedRoute)
} }
type mockOAuthTokenService struct { func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasources.DataSource, routes []*plugins.Route, path string, opts ...func(proxy *DataSourceProxy)) (*DataSourceProxy, error) {
token *oauth2.Token t.Helper()
oAuthEnabled bool
}
func (m *mockOAuthTokenService) GetCurrentOAuthToken(ctx context.Context, user identity.Requester) *oauth2.Token { cfg := setting.NewCfg()
return m.token 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 { tracer := tracing.InitializeTracerForTest()
return m.oAuthEnabled
}
func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) { proxy, err := NewDataSourceProxy(ds, routes, ctx, path, cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
return nil, false, nil if err != nil {
} return nil, err
}
func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *login.UserAuth) error { for _, o := range opts {
return nil o(proxy)
} }
func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *login.UserAuth) error { return proxy, nil
return nil
} }

View File

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

View File

@ -9,9 +9,13 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" 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/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
@ -20,8 +24,6 @@ import (
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPluginProxy(t *testing.T) { func TestPluginProxy(t *testing.T) {
@ -260,7 +262,7 @@ func TestPluginProxy(t *testing.T) {
ps := &pluginsettings.DTO{ ps := &pluginsettings.DTO{
SecureJSONData: map[string][]byte{}, 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) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -398,7 +400,7 @@ func TestPluginProxyRoutes(t *testing.T) {
ps := &pluginsettings.DTO{ ps := &pluginsettings.DTO{
SecureJSONData: map[string][]byte{}, 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) require.NoError(t, err)
proxy.HandleRequest() proxy.HandleRequest()
@ -429,7 +431,7 @@ func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsServic
ReqRole: org.RoleEditor, 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) require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "/api/plugin-proxy/grafana-simple-app/api/v4/alerts", nil) 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" "github.com/grafana/grafana/pkg/infra/tracing"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources" "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/oauthtoken"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
@ -26,7 +27,7 @@ import (
func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator validations.PluginRequestValidator, func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator validations.PluginRequestValidator,
pluginStore pluginstore.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider, pluginStore pluginstore.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider,
oauthTokenService *oauthtoken.Service, dsService datasources.DataSourceService, 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{ return &DataSourceProxyService{
DataSourceCache: dataSourceCache, DataSourceCache: dataSourceCache,
PluginRequestValidator: plugReqValidator, PluginRequestValidator: plugReqValidator,
@ -50,6 +51,7 @@ type DataSourceProxyService struct {
DataSourcesService datasources.DataSourceService DataSourcesService datasources.DataSourceService
tracer tracing.Tracer tracer tracing.Tracer
secretsService secrets.Service secretsService secrets.Service
fetures featuremgmt.FeatureToggles
} }
func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) { func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) {
@ -120,7 +122,7 @@ func (p *DataSourceProxyService) proxyDatasourceRequest(c *contextmodel.ReqConte
proxyPath := getProxyPath(c) proxyPath := getProxyPath(c)
proxy, err := pluginproxy.NewDataSourceProxy(ds, plugin.Routes, c, proxyPath, p.Cfg, p.HTTPClientProvider, 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 { if err != nil {
var urlValidationError datasource.URLValidationError var urlValidationError datasource.URLValidationError
if errors.As(err, &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)) middlewares = append(middlewares, clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features))
} }
if features.IsEnabled(featuremgmt.FlagIdForwarding) {
middlewares = append(middlewares, clientmiddleware.NewForwardIDMiddleware())
}
if cfg.SendUserHeader { if cfg.SendUserHeader {
middlewares = append(middlewares, clientmiddleware.NewUserHeaderMiddleware()) middlewares = append(middlewares, clientmiddleware.NewUserHeaderMiddleware())
} }

View File

@ -10,8 +10,12 @@ import (
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
) )
// UserHeaderName name of the header used when forwarding the Grafana user login. const (
const UserHeaderName = "X-Grafana-User" // 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. // PrepareProxyRequest prepares a request for being proxied.
// Removes X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, Origin, Referer headers. // 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()) 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)
}
}