Plugins: Refactor forward of cookies, OAuth token and header modifications by introducing client middlewares (#58132)
Adding support for backend plugin client middlewares. This allows headers in outgoing backend plugin and HTTP requests to be modified using client middlewares. The following client middlewares added: Forward cookies: Will forward incoming HTTP request Cookies to outgoing plugins.Client and HTTP requests if the datasource has enabled forwarding of cookies (keepCookies). Forward OAuth token: Will set OAuth token headers on outgoing plugins.Client and HTTP requests if the datasource has enabled Forward OAuth Identity (oauthPassThru). Clear auth headers: Will clear any outgoing HTTP headers that was part of the incoming HTTP request and used when authenticating to Grafana. The current suggested way to register client middlewares is to have a separate package, pluginsintegration, responsible for bootstrap/instantiate the backend plugin client with middlewares and/or longer term bootstrap/instantiate plugin management. Fixes #54135 Related to #47734 Related to #57870 Related to #41623 Related to #57065
This commit is contained in:
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -823,21 +822,6 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *datasource
|
||||
return response.Error(http.StatusForbidden, "Access denied", err)
|
||||
}
|
||||
|
||||
if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) {
|
||||
if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil {
|
||||
req.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if ok && idToken != "" {
|
||||
req.Headers["X-ID-Token"] = idToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxyutil.ClearCookieHeader(c.Req, ds.AllowedCookies(), []string{hs.Cfg.LoginCookieName})
|
||||
if cookieStr := c.Req.Header.Get("Cookie"); cookieStr != "" {
|
||||
req.Headers["Cookie"] = cookieStr
|
||||
}
|
||||
|
||||
resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), req)
|
||||
if err != nil {
|
||||
return translatePluginRequestErrorToAPIError(err)
|
||||
|
||||
@@ -3,7 +3,6 @@ package dtos
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -73,8 +72,6 @@ type MetricRequest struct {
|
||||
Debug bool `json:"debug"`
|
||||
|
||||
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
|
||||
|
||||
HTTPRequest *http.Request `json:"-"`
|
||||
}
|
||||
|
||||
func (mr *MetricRequest) GetUniqueDatasourceTypes() []string {
|
||||
@@ -98,11 +95,10 @@ func (mr *MetricRequest) GetUniqueDatasourceTypes() []string {
|
||||
|
||||
func (mr *MetricRequest) CloneWithQueries(queries []*simplejson.Json) MetricRequest {
|
||||
return MetricRequest{
|
||||
From: mr.From,
|
||||
To: mr.To,
|
||||
Queries: queries,
|
||||
Debug: mr.Debug,
|
||||
HTTPRequest: mr.HTTPRequest,
|
||||
From: mr.From,
|
||||
To: mr.To,
|
||||
Queries: queries,
|
||||
Debug: mr.Debug,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
|
||||
reqDTO.HTTPRequest = c.Req
|
||||
|
||||
resp, err := hs.queryDataService.QueryData(c.Req.Context(), c.SignedInUser, c.SkipCache, reqDTO)
|
||||
if err != nil {
|
||||
return hs.handleQueryMetricsError(err)
|
||||
|
||||
@@ -13,15 +13,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -46,31 +43,6 @@ func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request)
|
||||
return rv.err
|
||||
}
|
||||
|
||||
type fakeOAuthTokenService struct {
|
||||
passThruEnabled bool
|
||||
token *oauth2.Token
|
||||
}
|
||||
|
||||
func (ts *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *user.SignedInUser) *oauth2.Token {
|
||||
return ts.token
|
||||
}
|
||||
|
||||
func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) bool {
|
||||
return ts.passThruEnabled
|
||||
}
|
||||
|
||||
func (ts *fakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (ts *fakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *fakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// `/ds/query` endpoint test
|
||||
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
||||
qds := query.ProvideService(
|
||||
@@ -89,7 +61,6 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
||||
return &backend.QueryDataResponse{Responses: resp}, nil
|
||||
},
|
||||
},
|
||||
&fakeOAuthTokenService{},
|
||||
)
|
||||
serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.queryDataService = qds
|
||||
@@ -138,7 +109,6 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
||||
return &backend.QueryDataResponse{Responses: resp}, nil
|
||||
},
|
||||
},
|
||||
&fakeOAuthTokenService{},
|
||||
)
|
||||
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.queryDataService = qds
|
||||
@@ -292,7 +262,6 @@ func TestDataSourceQueryError(t *testing.T) {
|
||||
&fakePluginRequestValidator{},
|
||||
&fakeDatasources.FakeDataSourceService{},
|
||||
pluginClient.ProvideService(r, &config.Cfg{}),
|
||||
&fakeOAuthTokenService{},
|
||||
)
|
||||
hs.QuotaService = quotatest.New(false, nil)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -78,17 +76,6 @@ func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, plu
|
||||
return
|
||||
}
|
||||
|
||||
if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) {
|
||||
if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if ok && idToken != "" {
|
||||
req.Header.Add("X-ID-Token", idToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = hs.makePluginResourceRequest(c.Resp, req, pCtx); err != nil {
|
||||
handleCallResourceError(err, c)
|
||||
}
|
||||
@@ -110,24 +97,6 @@ func (hs *HTTPServer) pluginResourceRequest(c *models.ReqContext) (*http.Request
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
|
||||
keepCookieModel := struct {
|
||||
KeepCookies []string `json:"keepCookies"`
|
||||
}{}
|
||||
if dis := pCtx.DataSourceInstanceSettings; dis != nil {
|
||||
err := json.Unmarshal(dis.JSONData, &keepCookieModel)
|
||||
if err != nil {
|
||||
hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
|
||||
if list != nil {
|
||||
for _, name := range list.Items {
|
||||
req.Header.Del(name)
|
||||
}
|
||||
}
|
||||
|
||||
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies, []string{hs.Cfg.LoginCookieName})
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
@@ -315,11 +314,6 @@ func TestMakePluginResourceRequest(t *testing.T) {
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
const customHeader = "X-CUSTOM"
|
||||
req.Header.Set(customHeader, "val")
|
||||
ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
pCtx := backend.PluginContext{}
|
||||
err := hs.makePluginResourceRequest(resp, req, pCtx)
|
||||
@@ -333,7 +327,6 @@ func TestMakePluginResourceRequest(t *testing.T) {
|
||||
|
||||
require.Equal(t, resp.Header().Get("Content-Type"), "application/json")
|
||||
require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy"))
|
||||
require.Empty(t, req.Header.Get(customHeader))
|
||||
}
|
||||
|
||||
func TestMakePluginResourceRequestSetCookieNotPresent(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user