diff --git a/conf/defaults.ini b/conf/defaults.ini index 6447c22bddc..492525e6b5f 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -157,6 +157,9 @@ logging = false # How long the data proxy should wait before timing out default is 30 (seconds) timeout = 30 +# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false. +send_user_header = false + #################################### Analytics ########################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. diff --git a/conf/sample.ini b/conf/sample.ini index 1a574243f79..fd414c2af47 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -144,6 +144,9 @@ log_queries = # How long the data proxy should wait before timing out default is 30 (seconds) ;timeout = 30 +# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false. +;send_user_header = false + #################################### Analytics #################################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index d4ea0c05fb0..d94bacc5779 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -411,6 +411,22 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours).
+## [dataproxy] + +### logging + +This enables data proxy logging, default is false. + +### timeout + +How long the data proxy should wait before timing out default is 30 (seconds) + +### send_user_header + +If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false. + +
+ ## [analytics] ### reporting_enabled diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index d77d9d87b4a..abee55711b0 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -48,18 +48,18 @@ func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) { handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)) } } - handlers = append(handlers, AppPluginRoute(route, plugin.Id)) + handlers = append(handlers, AppPluginRoute(route, plugin.Id, hs)) r.Route(url, route.Method, handlers...) log.Debug("Plugins: Adding proxy route %s", url) } } } -func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler { +func AppPluginRoute(route *plugins.AppPluginRoute, appID string, hs *HTTPServer) macaron.Handler { return func(c *m.ReqContext) { path := c.Params("*") - proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID) + proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg) proxy.Transport = pluginProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 54a744fccdc..48f9c73c934 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -31,7 +31,7 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { // macaron does not include trailing slashes when resolving a wildcard path proxyPath := ensureProxyPathTrailingSlash(c.Req.URL.Path, c.Params("*")) - proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath) + proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath, hs.Cfg) proxy.HandleRequest() } diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b1950998297..3aec988f9e3 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -34,13 +34,14 @@ type DataSourceProxy struct { proxyPath string route *plugins.AppPluginRoute plugin *plugins.DataSourcePlugin + cfg *setting.Cfg } type httpClient interface { Do(req *http.Request) (*http.Response, error) } -func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy { +func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string, cfg *setting.Cfg) *DataSourceProxy { targetURL, _ := url.Parse(ds.Url) return &DataSourceProxy{ @@ -49,6 +50,7 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx ctx: ctx, proxyPath: proxyPath, targetUrl: targetURL, + cfg: cfg, } } @@ -170,6 +172,10 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { req.Header.Add("Authorization", dsAuth) } + if proxy.cfg.SendUserHeader && !proxy.ctx.SignedInUser.IsAnonymous { + req.Header.Add("X-Grafana-User", proxy.ctx.SignedInUser.Login) + } + // clear cookie header, except for whitelisted cookies var keptCookies []*http.Cookie if proxy.ds.JsonData != nil { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index c9be169565f..bfad7d5670d 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -81,7 +81,7 @@ func TestDSRouteRule(t *testing.T) { } Convey("When matching route path", func() { - proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method") + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) proxy.route = plugin.Routes[0] ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) @@ -92,7 +92,7 @@ func TestDSRouteRule(t *testing.T) { }) Convey("When matching route path and has dynamic url", func() { - proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method") + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{}) proxy.route = plugin.Routes[3] ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) @@ -104,20 +104,20 @@ func TestDSRouteRule(t *testing.T) { Convey("Validating request", func() { Convey("plugin route with valid role", func() { - proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method") + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) err := proxy.validateRequest() So(err, ShouldBeNil) }) Convey("plugin route with admin role and user is editor", func() { - proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin") + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) err := proxy.validateRequest() So(err, ShouldNotBeNil) }) Convey("plugin route with admin role and user is admin", func() { ctx.SignedInUser.OrgRole = m.ROLE_ADMIN - proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin") + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) err := proxy.validateRequest() So(err, ShouldBeNil) }) @@ -186,7 +186,7 @@ func TestDSRouteRule(t *testing.T) { So(err, ShouldBeNil) client = newFakeHTTPClient(json) - proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1") + proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) proxy1.route = plugin.Routes[0] ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds) @@ -200,7 +200,7 @@ func TestDSRouteRule(t *testing.T) { req, _ := http.NewRequest("GET", "http://localhost/asd", nil) client = newFakeHTTPClient(json2) - proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2") + proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{}) proxy2.route = plugin.Routes[1] ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds) @@ -215,7 +215,7 @@ func TestDSRouteRule(t *testing.T) { req, _ := http.NewRequest("GET", "http://localhost/asd", nil) client = newFakeHTTPClient([]byte{}) - proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1") + proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) proxy3.route = plugin.Routes[0] ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds) @@ -236,7 +236,7 @@ func TestDSRouteRule(t *testing.T) { ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "/render") + proxy := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) So(err, ShouldBeNil) @@ -261,7 +261,7 @@ func TestDSRouteRule(t *testing.T) { } ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "") + proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) So(err, ShouldBeNil) @@ -291,7 +291,7 @@ func TestDSRouteRule(t *testing.T) { } ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "") + proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) requestURL, _ := url.Parse("http://grafana.com/sub") req := http.Request{URL: requestURL, Header: make(http.Header)} @@ -317,7 +317,7 @@ func TestDSRouteRule(t *testing.T) { } ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "") + proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) requestURL, _ := url.Parse("http://grafana.com/sub") req := http.Request{URL: requestURL, Header: make(http.Header)} @@ -347,7 +347,7 @@ func TestDSRouteRule(t *testing.T) { } ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "") + proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) requestURL, _ := url.Parse("http://grafana.com/sub") req := http.Request{URL: requestURL, Header: make(http.Header)} @@ -369,7 +369,7 @@ func TestDSRouteRule(t *testing.T) { Url: "http://host/root/", } ctx := &m.ReqContext{} - proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/") + proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) req.Header.Add("Origin", "grafana.com") req.Header.Add("Referer", "grafana.com") @@ -388,9 +388,68 @@ func TestDSRouteRule(t *testing.T) { So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere") }) }) + + Convey("When SendUserHeader config is enabled", func() { + req := getDatasourceProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: true}, + ) + Convey("Should add header with username", func() { + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user") + }) + }) + + Convey("When SendUserHeader config is disabled", func() { + req := getDatasourceProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: false}, + ) + Convey("Should not add header with username", func() { + // Get will return empty string even if header is not set + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") + }) + }) + + Convey("When SendUserHeader config is enabled but user is anonymous", func() { + req := getDatasourceProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{IsAnonymous: true}, + }, + &setting.Cfg{SendUserHeader: true}, + ) + Convey("Should not add header with username", func() { + // Get will return empty string even if header is not set + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") + }) + }) }) } +// getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. +func getDatasourceProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request { + plugin := &plugins.DataSourcePlugin{} + + ds := &m.DataSource{ + Type: "custom", + Url: "http://host/root/", + } + + proxy := NewDataSourceProxy(ds, plugin, ctx, "", cfg) + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + So(err, ShouldBeNil) + + proxy.getDirector()(req) + return req +} + type httpClientStub struct { fakeBody []byte } diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index 4cf1dbd7cde..5ee59017a82 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -2,6 +2,7 @@ package pluginproxy import ( "encoding/json" + "github.com/grafana/grafana/pkg/setting" "net" "net/http" "net/http/httputil" @@ -37,7 +38,7 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http. return result, err } -func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string) *httputil.ReverseProxy { +func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy { targetURL, _ := url.Parse(route.Url) director := func(req *http.Request) { @@ -79,6 +80,10 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl req.Header.Add("X-Grafana-Context", string(ctxJson)) + if cfg.SendUserHeader && !ctx.SignedInUser.IsAnonymous { + req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login) + } + if len(route.Headers) > 0 { headers, err := getHeaders(route, ctx.OrgId, appID) if err != nil { diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index 424c3fd670c..e4a4fdb25ba 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -1,6 +1,7 @@ package pluginproxy import ( + "net/http" "testing" "github.com/grafana/grafana/pkg/bus" @@ -44,4 +45,59 @@ func TestPluginProxy(t *testing.T) { }) }) + Convey("When SendUserHeader config is enabled", t, func() { + req := getPluginProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: true}, + ) + + Convey("Should add header with username", func() { + // Get will return empty string even if header is not set + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user") + }) + }) + + Convey("When SendUserHeader config is disabled", t, func() { + req := getPluginProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: false}, + ) + Convey("Should not add header with username", func() { + // Get will return empty string even if header is not set + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") + }) + }) + + Convey("When SendUserHeader config is enabled but user is anonymous", t, func() { + req := getPluginProxiedRequest( + &m.ReqContext{ + SignedInUser: &m.SignedInUser{IsAnonymous: true}, + }, + &setting.Cfg{SendUserHeader: true}, + ) + + Convey("Should not add header with username", func() { + // Get will return empty string even if header is not set + So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") + }) + }) +} + +// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. +func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request { + route := &plugins.AppPluginRoute{} + proxy := NewApiPluginProxy(ctx, "", route, "", cfg) + + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + So(err, ShouldBeNil) + proxy.Director(req) + return req } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index ac16cc73e9c..bc57291b5f9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -242,6 +242,9 @@ type Cfg struct { // User EditorsCanOwn bool + // Dataproxy + SendUserHeader bool + // DistributedCache RemoteCacheOptions *RemoteCacheOptions } @@ -604,6 +607,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { dataproxy := iniFile.Section("dataproxy") DataProxyLogging = dataproxy.Key("logging").MustBool(false) DataProxyTimeout = dataproxy.Key("timeout").MustInt(30) + cfg.SendUserHeader = dataproxy.Key("send_user_header").MustBool(false) // read security settings security := iniFile.Section("security")