Commit e503188b by Arve Knudsen Committed by GitHub

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>
parent fc7edab8
......@@ -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
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)
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()
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.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
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)
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()
}
keepCookieNames := []string{}
if proxy.ds.JsonData != nil {
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
keepCookieNames = keepCookies.MustStringArray()
}
}
proxyutil.ClearCookieHeader(req, keepCookieNames)
proxyutil.PrepareProxyRequest(req)
proxyutil.ClearCookieHeader(req, keepCookieNames)
proxyutil.PrepareProxyRequest(req)
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
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")
// 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 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)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment