فهرست منبع

Add custom header with grafana user and a config switch for it

Andrej Ocenas 6 سال پیش
والد
کامیت
bbdc1c0e64

+ 3 - 0
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 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.

+ 3 - 0
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 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.

+ 3 - 3
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)
 	}

+ 1 - 1
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()
 }
 

+ 7 - 1
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 {
+			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 {

+ 60 - 14
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,55 @@ 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, "")
+			})
+		})
 	})
 }
 
+// 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
 }

+ 6 - 1
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 {
+			req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login)
+		}
+
 		if len(route.Headers) > 0 {
 			headers, err := getHeaders(route, ctx.OrgId, appID)
 			if err != nil {

+ 42 - 0
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,45 @@ 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, "")
+		})
+	})
+}
+
+// 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
 }

+ 4 - 0
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")