mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Data source proxy: Convert 401 from data source to 400 (#28962)
* Data source proxy: Convert 401 from data source to 400 Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
fc7edab8fd
commit
e503188b6f
@ -98,13 +98,8 @@ func (proxy *DataSourceProxy) HandleRequest() {
|
||||
return
|
||||
}
|
||||
|
||||
proxyErrorLogger := logger.New("userId", proxy.ctx.UserId, "orgId", proxy.ctx.OrgId, "uname", proxy.ctx.Login, "path", proxy.ctx.Req.URL.Path, "remote_addr", proxy.ctx.RemoteAddr(), "referer", proxy.ctx.Req.Referer())
|
||||
|
||||
reverseProxy := &httputil.ReverseProxy{
|
||||
Director: proxy.getDirector(),
|
||||
FlushInterval: time.Millisecond * 200,
|
||||
ErrorLog: log.New(&logWrapper{logger: proxyErrorLogger}, "", 0),
|
||||
}
|
||||
proxyErrorLogger := logger.New("userId", proxy.ctx.UserId, "orgId", proxy.ctx.OrgId, "uname", proxy.ctx.Login,
|
||||
"path", proxy.ctx.Req.URL.Path, "remote_addr", proxy.ctx.RemoteAddr(), "referer", proxy.ctx.Req.Referer())
|
||||
|
||||
transport, err := proxy.ds.GetHttpTransport()
|
||||
if err != nil {
|
||||
@ -112,16 +107,43 @@ func (proxy *DataSourceProxy) HandleRequest() {
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy.Transport = &handleResponseTransport{
|
||||
transport: transport,
|
||||
reverseProxy := &httputil.ReverseProxy{
|
||||
Director: proxy.director,
|
||||
FlushInterval: time.Millisecond * 200,
|
||||
ErrorLog: log.New(&logWrapper{logger: proxyErrorLogger}, "", 0),
|
||||
Transport: &handleResponseTransport{
|
||||
transport: transport,
|
||||
},
|
||||
ModifyResponse: func(resp *http.Response) error {
|
||||
if resp.StatusCode == 401 {
|
||||
// The data source rejected the request as unauthorized, convert to 400 (bad request)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read data source response body: %w", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
proxyErrorLogger.Info("Authentication to data source failed", "body", string(body), "statusCode",
|
||||
resp.StatusCode)
|
||||
msg := "Authentication to data source failed"
|
||||
*resp = http.Response{
|
||||
StatusCode: 400,
|
||||
Status: "Bad Request",
|
||||
Body: ioutil.NopCloser(strings.NewReader(msg)),
|
||||
ContentLength: int64(len(msg)),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
proxy.logRequest()
|
||||
|
||||
span, ctx := opentracing.StartSpanFromContext(proxy.ctx.Req.Context(), "datasource reverse proxy")
|
||||
defer span.Finish()
|
||||
|
||||
proxy.ctx.Req.Request = proxy.ctx.Req.WithContext(ctx)
|
||||
|
||||
defer span.Finish()
|
||||
span.SetTag("datasource_id", proxy.ds.Id)
|
||||
span.SetTag("datasource_type", proxy.ds.Type)
|
||||
span.SetTag("user_id", proxy.ctx.SignedInUser.UserId)
|
||||
@ -148,70 +170,65 @@ func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, hea
|
||||
}
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = proxy.targetUrl.Scheme
|
||||
req.URL.Host = proxy.targetUrl.Host
|
||||
req.Host = proxy.targetUrl.Host
|
||||
func (proxy *DataSourceProxy) director(req *http.Request) {
|
||||
req.URL.Scheme = proxy.targetUrl.Scheme
|
||||
req.URL.Host = proxy.targetUrl.Host
|
||||
req.Host = proxy.targetUrl.Host
|
||||
|
||||
reqQueryVals := req.URL.Query()
|
||||
reqQueryVals := req.URL.Query()
|
||||
|
||||
switch proxy.ds.Type {
|
||||
case models.DS_INFLUXDB_08:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
|
||||
reqQueryVals.Add("u", proxy.ds.User)
|
||||
reqQueryVals.Add("p", proxy.ds.DecryptedPassword())
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
case models.DS_INFLUXDB:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
if !proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.DecryptedPassword()))
|
||||
}
|
||||
default:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
switch proxy.ds.Type {
|
||||
case models.DS_INFLUXDB_08:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
|
||||
reqQueryVals.Add("u", proxy.ds.User)
|
||||
reqQueryVals.Add("p", proxy.ds.DecryptedPassword())
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
case models.DS_INFLUXDB:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
if !proxy.ds.BasicAuth {
|
||||
req.Header.Set("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.DecryptedPassword()))
|
||||
}
|
||||
default:
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
}
|
||||
|
||||
if proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.DecryptedBasicAuthPassword()))
|
||||
if proxy.ds.BasicAuth {
|
||||
req.Header.Set("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser,
|
||||
proxy.ds.DecryptedBasicAuthPassword()))
|
||||
}
|
||||
|
||||
dsAuth := req.Header.Get("X-DS-Authorization")
|
||||
if len(dsAuth) > 0 {
|
||||
req.Header.Del("X-DS-Authorization")
|
||||
req.Header.Set("Authorization", dsAuth)
|
||||
}
|
||||
|
||||
applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
|
||||
|
||||
keepCookieNames := []string{}
|
||||
if proxy.ds.JsonData != nil {
|
||||
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||
keepCookieNames = keepCookies.MustStringArray()
|
||||
}
|
||||
}
|
||||
|
||||
dsAuth := req.Header.Get("X-DS-Authorization")
|
||||
if len(dsAuth) > 0 {
|
||||
req.Header.Del("X-DS-Authorization")
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", dsAuth)
|
||||
}
|
||||
proxyutil.ClearCookieHeader(req, keepCookieNames)
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
|
||||
applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
||||
|
||||
keepCookieNames := []string{}
|
||||
if proxy.ds.JsonData != nil {
|
||||
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||
keepCookieNames = keepCookies.MustStringArray()
|
||||
}
|
||||
}
|
||||
// Clear Origin and Referer to avoir CORS issues
|
||||
req.Header.Del("Origin")
|
||||
req.Header.Del("Referer")
|
||||
|
||||
proxyutil.ClearCookieHeader(req, keepCookieNames)
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
if proxy.route != nil {
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
||||
|
||||
// Clear Origin and Referer to avoir CORS issues
|
||||
req.Header.Del("Origin")
|
||||
req.Header.Del("Referer")
|
||||
|
||||
if proxy.route != nil {
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
}
|
||||
|
||||
if oauthtoken.IsOAuthPassThruEnabled(proxy.ds) {
|
||||
if token := oauthtoken.GetCurrentOAuthToken(proxy.ctx.Req.Context(), proxy.ctx.SignedInUser); token != nil {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
}
|
||||
if oauthtoken.IsOAuthPassThruEnabled(proxy.ds) {
|
||||
if token := oauthtoken.GetCurrentOAuthToken(proxy.ctx.Req.Context(), proxy.ctx.SignedInUser); token != nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -295,7 +295,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
|
||||
t.Run("Can translate request URL and path", func(t *testing.T) {
|
||||
assert.Equal(t, "graphite:8080", req.URL.Host)
|
||||
@ -322,7 +322,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
assert.Equal(t, "/db/site/", req.URL.Path)
|
||||
})
|
||||
|
||||
@ -348,7 +348,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
proxy.director(&req)
|
||||
|
||||
assert.Equal(t, "", req.Header.Get("Cookie"))
|
||||
})
|
||||
@ -375,7 +375,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
proxy.director(&req)
|
||||
|
||||
assert.Equal(t, "JSESSION_ID=test", req.Header.Get("Cookie"))
|
||||
})
|
||||
@ -395,7 +395,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
req.Header.Add("X-Canary", "stillthere")
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
|
||||
assert.Equal(t, "http://host/root/path/to/folder/", req.URL.String())
|
||||
|
||||
@ -404,7 +404,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
assert.Equal(t, "stillthere", req.Header.Get("X-Canary"))
|
||||
})
|
||||
|
||||
t.Run("When proxying a datasource that has oauth token pass-through enabled", func(t *testing.T) {
|
||||
t.Run("When proxying a datasource that has OAuth token pass-through enabled", func(t *testing.T) {
|
||||
social.SocialMap["generic_oauth"] = &social.SocialGenericOAuth{
|
||||
SocialBase: &social.SocialBase{
|
||||
Config: &oauth2.Config{},
|
||||
@ -453,7 +453,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
|
||||
assert.Equal(t, "Bearer testtoken", req.Header.Get("Authorization"))
|
||||
})
|
||||
@ -517,66 +517,112 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
runDatasourceAuthTest(t, test)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// test DataSourceProxy request handling.
|
||||
func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
var writeErr error
|
||||
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
type setUpCfg struct {
|
||||
headers map[string]string
|
||||
writeCb func(w http.ResponseWriter)
|
||||
}
|
||||
|
||||
setUp := func(t *testing.T, cfgs ...setUpCfg) (*models.ReqContext, *models.DataSource) {
|
||||
writeErr = nil
|
||||
|
||||
t.Run("HandleRequest()", func(t *testing.T) {
|
||||
var writeErr error
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"})
|
||||
w.WriteHeader(200)
|
||||
_, writeErr = w.Write([]byte("I am the backend"))
|
||||
written := false
|
||||
for _, cfg := range cfgs {
|
||||
if cfg.writeCb != nil {
|
||||
t.Log("Writing response via callback")
|
||||
cfg.writeCb(w)
|
||||
written = true
|
||||
}
|
||||
}
|
||||
if !written {
|
||||
t.Log("Writing default response")
|
||||
w.WriteHeader(200)
|
||||
_, writeErr = w.Write([]byte("I am the backend"))
|
||||
}
|
||||
}))
|
||||
t.Cleanup(backend.Close)
|
||||
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &models.DataSource{Url: backend.URL, Type: models.DS_GRAPHITE}
|
||||
|
||||
responseRecorder := &CloseNotifierResponseRecorder{
|
||||
responseRecorder := &closeNotifierResponseRecorder{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
t.Cleanup(responseRecorder.Close)
|
||||
|
||||
setupCtx := func(fn func(http.ResponseWriter)) *models.ReqContext {
|
||||
responseWriter := macaron.NewResponseWriter("GET", responseRecorder)
|
||||
if fn != nil {
|
||||
fn(responseWriter)
|
||||
}
|
||||
responseWriter := macaron.NewResponseWriter("GET", responseRecorder)
|
||||
|
||||
return &models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
Context: &macaron.Context{
|
||||
Req: macaron.Request{
|
||||
Request: httptest.NewRequest("GET", "/render", nil),
|
||||
},
|
||||
Resp: responseWriter,
|
||||
},
|
||||
// XXX: Really unsure why, but setting headers within the HTTP handler function doesn't stick,
|
||||
// so doing it here instead
|
||||
for _, cfg := range cfgs {
|
||||
for k, v := range cfg.headers {
|
||||
responseWriter.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
|
||||
writeErr = nil
|
||||
ctx := setupCtx(nil)
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
return &models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
Context: &macaron.Context{
|
||||
Req: macaron.Request{
|
||||
Request: httptest.NewRequest("GET", "/render", nil),
|
||||
},
|
||||
Resp: responseWriter,
|
||||
},
|
||||
}, ds
|
||||
}
|
||||
|
||||
proxy.HandleRequest()
|
||||
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Empty(t, proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
proxy.HandleRequest()
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Empty(t, proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
})
|
||||
|
||||
t.Run("When response header Set-Cookie is set should remove proxied Set-Cookie header and restore the original Set-Cookie header", func(t *testing.T) {
|
||||
ctx, ds := setUp(t, setUpCfg{
|
||||
headers: map[string]string{
|
||||
"Set-Cookie": "important_cookie=important_value",
|
||||
},
|
||||
})
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("When response header Set-Cookie is set should remove proxied Set-Cookie header and restore the original Set-Cookie header", func(t *testing.T) {
|
||||
writeErr = nil
|
||||
ctx := setupCtx(func(w http.ResponseWriter) {
|
||||
w.Header().Set("Set-Cookie", "important_cookie=important_value")
|
||||
})
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
proxy.HandleRequest()
|
||||
|
||||
proxy.HandleRequest()
|
||||
require.NoError(t, writeErr)
|
||||
assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
})
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
t.Run("Data source returns status code 401", func(t *testing.T) {
|
||||
ctx, ds := setUp(t, setUpCfg{
|
||||
writeCb: func(w http.ResponseWriter) {
|
||||
w.WriteHeader(401)
|
||||
w.Header().Set("www-authenticate", `Basic realm="Access to the server"`)
|
||||
_, err := w.Write([]byte("Not authenticated"))
|
||||
require.NoError(t, err)
|
||||
t.Log("Wrote 401 response")
|
||||
},
|
||||
})
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Equal(t, 400, proxy.ctx.Resp.Status(), "Status code 401 should be converted to 400")
|
||||
assert.Empty(t, proxy.ctx.Resp.Header().Get("www-authenticate"))
|
||||
})
|
||||
}
|
||||
|
||||
@ -667,17 +713,17 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type CloseNotifierResponseRecorder struct {
|
||||
type closeNotifierResponseRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closeChan chan bool
|
||||
}
|
||||
|
||||
func (r *CloseNotifierResponseRecorder) CloseNotify() <-chan bool {
|
||||
func (r *closeNotifierResponseRecorder) CloseNotify() <-chan bool {
|
||||
r.closeChan = make(chan bool)
|
||||
return r.closeChan
|
||||
}
|
||||
|
||||
func (r *CloseNotifierResponseRecorder) Close() {
|
||||
func (r *closeNotifierResponseRecorder) Close() {
|
||||
close(r.closeChan)
|
||||
}
|
||||
|
||||
@ -695,7 +741,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
return req
|
||||
}
|
||||
|
||||
@ -806,7 +852,7 @@ func runDatasourceAuthTest(t *testing.T, test *testCase) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
proxy.director(req)
|
||||
|
||||
test.checkReq(req)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user