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)
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)