diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index 71d1f5c8479..7c0e398bcf5 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -293,7 +293,7 @@ When configured, Grafana will pass the user's token to the plugin in an Authoriz ```go func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - token := strings.Fields(req.Headers["Authorization"]) + token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName)) var ( tokenType = token[0] accessToken = token[1] @@ -304,7 +304,7 @@ func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthR } func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - token := strings.Fields(req.Headers["Authorization"]) + token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName)) var ( tokenType = token[0] accessToken = token[1] @@ -320,14 +320,14 @@ In addition, if the user's token includes an ID token, Grafana will pass the use ```go func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - idToken := req.Headers["X-ID-Token"] + idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) // ... return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil } func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - idToken := req.Headers["X-ID-Token"] + idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) for _, q := range req.Queries { // ... @@ -339,8 +339,8 @@ The `Authorization` and `X-ID-Token` headers will also be available on the `Call ```go func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - token := req.Headers["Authorization"] - idToken := req.Headers["X-ID-Token"] // present if user's token includes an ID token + token := req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName) + idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) // present if user's token includes an ID token // ... } @@ -356,19 +356,43 @@ When configured, Grafana will pass these cookies to the plugin in the `Cookie` h ```go func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - cookies:= req.Headers["Cookie"] + cookies:= req.GetHTTPHeader(backend.CookiesHeaderName) // ... } func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - cookies := req.Headers["Cookie"] + cookies:= req.GetHTTPHeader(backend.CookiesHeaderName) // ... } func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - cookies:= req.Headers["Cookie"] + cookies:= req.GetHTTPHeader(backend.CookiesHeaderName) + + // ... +} +``` + +## Forward user header for the logged-in user + +When [send_user_header]({{< relref "../../setup-grafana/configure-grafana/_index.md#send_user_header" >}}) is enabled, Grafana will pass the user header to the plugin in the `X-Grafana-User` header, available in the `QueryData`, `CallResource` and `CheckHealth` requests in your backend data source. + +```go +func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + u := req.GetHTTPHeader("X-Grafana-User") + + // ... +} + +func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + u := req.GetHTTPHeader("X-Grafana-User") + + // ... +} + +func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + u := req.GetHTTPHeader("X-Grafana-User") // ... } diff --git a/go.mod b/go.mod index 576b355b678..82a7c7e4942 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/grafana/cuetsy v0.1.1 github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.3.1 - github.com/grafana/grafana-plugin-sdk-go v0.145.0 + github.com/grafana/grafana-plugin-sdk-go v0.147.0 github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.0.0 @@ -73,7 +73,7 @@ require ( github.com/lib/pq v1.10.7 github.com/linkedin/goavro/v2 v2.10.0 github.com/m3db/prometheus_remote_client_golang v0.4.4 - github.com/magefile/mage v1.13.0 + github.com/magefile/mage v1.14.0 github.com/mattn/go-isatty v0.0.14 github.com/mattn/go-sqlite3 v1.14.16 github.com/matttproud/golang_protobuf_extensions v1.0.2 @@ -268,6 +268,8 @@ require ( k8s.io/client-go v12.0.0+incompatible // gets replaced with v0.25.0 ) +require k8s.io/apimachinery v0.25.0 + require ( cloud.google.com/go v0.102.0 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect @@ -318,7 +320,6 @@ require ( gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.25.0 // indirect - k8s.io/apimachinery v0.25.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect diff --git a/go.sum b/go.sum index 47212ee0291..e5b33e0db0b 100644 --- a/go.sum +++ b/go.sum @@ -1384,8 +1384,8 @@ github.com/grafana/grafana-azure-sdk-go v1.3.1/go.mod h1:rgrnK9m6CgKlgx4rH3FFP/6 github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.145.0 h1:ZlRxxV3C6RA+wNWeGr+rLVD70pgsZwiLI9etzE0zu+Q= -github.com/grafana/grafana-plugin-sdk-go v0.145.0/go.mod h1:dFof/7GenWBFTmrfcPRCpLau7tgIED0ykzupWAlB0o0= +github.com/grafana/grafana-plugin-sdk-go v0.147.0 h1:VavvJOa/Ubs+wzalzWIl+FQmdaD4vEK8KVYU0a8rf+E= +github.com/grafana/grafana-plugin-sdk-go v0.147.0/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q= github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293 h1:dJIdfHqu+XjKz+w9zXLqXKPdp6Jjx/UPSOwdeSfWdeQ= github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4= @@ -1782,8 +1782,8 @@ github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkV github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8= github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc= github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/magefile/mage v1.13.0 h1:XtLJl8bcCM7EFoO8FyH8XK3t7G5hQAeK+i4tq+veT9M= -github.com/magefile/mage v1.13.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index c48f6a900ad..ba450c9f056 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -222,7 +222,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) { req.Header.Set("Authorization", dsAuth) } - applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) + proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies(), []string{proxy.cfg.LoginCookieName}) req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index d89c5c78a4f..e77b83d0cb3 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -154,7 +154,7 @@ func (proxy PluginProxy) director(req *http.Request) { req.Header.Set("X-Grafana-Context", string(ctxJSON)) - applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) + proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) if err := addHeaders(&req.Header, proxy.matchedRoute, data); err != nil { proxy.ctx.JsonApiErr(500, "Failed to render plugin headers", err) diff --git a/pkg/api/pluginproxy/utils.go b/pkg/api/pluginproxy/utils.go index 05e9c5a33c8..0ad7669bcc5 100644 --- a/pkg/api/pluginproxy/utils.go +++ b/pkg/api/pluginproxy/utils.go @@ -9,7 +9,6 @@ import ( "text/template" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/user" ) // interpolateString accepts template data and return a string with substitutions @@ -84,11 +83,3 @@ func setBodyContent(req *http.Request, route *plugins.Route, data templateData) return nil } - -// Set the X-Grafana-User header if needed (and remove if not) -func applyUserHeader(sendUserHeader bool, req *http.Request, user *user.SignedInUser) { - req.Header.Del("X-Grafana-User") - if sendUserHeader && !user.IsAnonymous { - req.Header.Set("X-Grafana-User", user.Login) - } -} diff --git a/pkg/plugins/backendplugin/coreplugin/registry.go b/pkg/plugins/backendplugin/coreplugin/registry.go index 2902c5883cd..ec8cac5e649 100644 --- a/pkg/plugins/backendplugin/coreplugin/registry.go +++ b/pkg/plugins/backendplugin/coreplugin/registry.go @@ -148,3 +148,8 @@ func (l *logWrapper) Error(msg string, args ...interface{}) { func (l *logWrapper) Level() sdklog.Level { return sdklog.NoLevel } + +func (l *logWrapper) With(args ...interface{}) sdklog.Logger { + l.logger = l.logger.New(args...) + return l +} diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go new file mode 100644 index 00000000000..65cd4266cc4 --- /dev/null +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go @@ -0,0 +1,103 @@ +package clientmiddleware + +import ( + "context" + "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/util/proxyutil" +) + +// NewUserHeaderMiddleware creates a new plugins.ClientMiddleware that will +// populate the X-Grafana-User header on outgoing plugins.Client and HTTP +// requests. +func NewUserHeaderMiddleware() plugins.ClientMiddleware { + return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + return &UserHeaderMiddleware{ + next: next, + } + }) +} + +type UserHeaderMiddleware struct { + next plugins.Client +} + +func (m *UserHeaderMiddleware) applyToken(ctx context.Context, pCtx backend.PluginContext, h backend.ForwardHTTPHeaders) context.Context { + reqCtx := contexthandler.FromContext(ctx) + // if no HTTP request context skip middleware + if h == nil || reqCtx == nil || reqCtx.Req == nil || reqCtx.SignedInUser == nil { + return ctx + } + + h.DeleteHTTPHeader(proxyutil.UserHeaderName) + if !reqCtx.IsAnonymous { + h.SetHTTPHeader(proxyutil.UserHeaderName, reqCtx.Login) + } + + middlewares := []sdkhttpclient.Middleware{} + + if !reqCtx.IsAnonymous { + httpHeaders := http.Header{ + proxyutil.UserHeaderName: []string{reqCtx.Login}, + } + + middlewares = append(middlewares, httpclientprovider.SetHeadersMiddleware(httpHeaders)) + } else { + middlewares = append(middlewares, httpclientprovider.DeleteHeadersMiddleware(proxyutil.UserHeaderName)) + } + + ctx = sdkhttpclient.WithContextualMiddleware(ctx, middlewares...) + + return ctx +} + +func (m *UserHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if req == nil { + return m.next.QueryData(ctx, req) + } + + ctx = m.applyToken(ctx, req.PluginContext, req) + + return m.next.QueryData(ctx, req) +} + +func (m *UserHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + if req == nil { + return m.next.CallResource(ctx, req, sender) + } + + ctx = m.applyToken(ctx, req.PluginContext, req) + + return m.next.CallResource(ctx, req, sender) +} + +func (m *UserHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + if req == nil { + return m.next.CheckHealth(ctx, req) + } + + ctx = m.applyToken(ctx, req.PluginContext, req) + + return m.next.CheckHealth(ctx, req) +} + +func (m *UserHeaderMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { + return m.next.CollectMetrics(ctx, req) +} + +func (m *UserHeaderMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { + return m.next.SubscribeStream(ctx, req) +} + +func (m *UserHeaderMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { + return m.next.PublishStream(ctx, req) +} + +func (m *UserHeaderMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { + return m.next.RunStream(ctx, req, sender) +} diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go new file mode 100644 index 00000000000..8b49e1c0f7a --- /dev/null +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go @@ -0,0 +1,254 @@ +package clientmiddleware + +import ( + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" + "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/proxyutil" + "github.com/stretchr/testify/require" +) + +func TestUserHeaderMiddleware(t *testing.T) { + t.Run("When anononymous user in reqContext", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) + + t.Run("And requests are for a datasource", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + ) + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } + + t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Empty(t, cdt.QueryDataReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.QueryDataCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { + err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{}, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Empty(t, cdt.CallResourceReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CallResourceCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Empty(t, cdt.CheckHealthReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CheckHealthCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + }) + + t.Run("And requests are for an app", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + ) + + pluginCtx := backend.PluginContext{ + AppInstanceSettings: &backend.AppInstanceSettings{}, + } + + t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Empty(t, cdt.QueryDataReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.QueryDataCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { + err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{}, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Empty(t, cdt.CallResourceReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CallResourceCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Empty(t, cdt.CheckHealthReq.Headers) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CheckHealthCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.DeleteHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + }) + }) + + t.Run("When real user in reqContext", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) + + t.Run("And requests are for a datasource", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + Login: "admin", + }), + clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + ) + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } + + t.Run("Should forward user header when calling QueryData", func(t *testing.T) { + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "admin", cdt.QueryDataReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.QueryDataCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should forward user header when calling CallResource", func(t *testing.T) { + err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{}, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, "admin", cdt.CallResourceReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CallResourceCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "admin", cdt.CheckHealthReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CheckHealthCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + }) + + t.Run("And requests are for an app", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + Login: "admin", + }), + clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + ) + + pluginCtx := backend.PluginContext{ + AppInstanceSettings: &backend.AppInstanceSettings{}, + } + + t.Run("Should forward user header when calling QueryData", func(t *testing.T) { + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "admin", cdt.QueryDataReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.QueryDataCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should forward user header when calling CallResource", func(t *testing.T) { + err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{}, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, "admin", cdt.CallResourceReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CallResourceCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + + t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "admin", cdt.CheckHealthReq.GetHTTPHeader(proxyutil.UserHeaderName)) + + middlewares := httpclient.ContextualMiddlewareFromContext(cdt.CheckHealthCtx) + require.Len(t, middlewares, 1) + require.Equal(t, httpclientprovider.SetHeadersMiddlewareName, middlewares[0].(httpclient.MiddlewareName).MiddlewareName()) + }) + }) + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index cf9c38e76e5..efe42665e81 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -77,5 +77,9 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken clientmiddleware.NewCookiesMiddleware(skipCookiesNames), } + if cfg.SendUserHeader { + middlewares = append(middlewares, clientmiddleware.NewUserHeaderMiddleware()) + } + return middlewares } diff --git a/pkg/util/proxyutil/proxyutil.go b/pkg/util/proxyutil/proxyutil.go index 50897634838..68ae725919a 100644 --- a/pkg/util/proxyutil/proxyutil.go +++ b/pkg/util/proxyutil/proxyutil.go @@ -4,8 +4,13 @@ import ( "net" "net/http" "sort" + + "github.com/grafana/grafana/pkg/services/user" ) +// UserHeaderName name of the header used when forwarding the Grafana user login. +const UserHeaderName = "X-Grafana-User" + // PrepareProxyRequest prepares a request for being proxied. // Removes X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, Origin, Referer headers. // Set X-Grafana-Referer based on contents of Referer. @@ -69,3 +74,11 @@ func ClearCookieHeader(req *http.Request, keepCookiesNames []string, skipCookies func SetProxyResponseHeaders(header http.Header) { header.Set("Content-Security-Policy", "sandbox") } + +// ApplyUserHeader Set the X-Grafana-User header if needed (and remove if not). +func ApplyUserHeader(sendUserHeader bool, req *http.Request, user *user.SignedInUser) { + req.Header.Del(UserHeaderName) + if sendUserHeader && user != nil && !user.IsAnonymous { + req.Header.Set(UserHeaderName, user.Login) + } +} diff --git a/pkg/util/proxyutil/proxyutil_test.go b/pkg/util/proxyutil/proxyutil_test.go index 209c7a01fac..95cbf69cf23 100644 --- a/pkg/util/proxyutil/proxyutil_test.go +++ b/pkg/util/proxyutil/proxyutil_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -109,3 +110,39 @@ func TestClearCookieHeader(t *testing.T) { require.Equal(t, "cookie1=", req.Header.Get("Cookie")) }) } + +func TestApplyUserHeader(t *testing.T) { + t.Run("Should not apply user header when not enabled, should remove the existing", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + req.Header.Set("X-Grafana-User", "admin") + + ApplyUserHeader(false, req, &user.SignedInUser{Login: "admin"}) + require.NotContains(t, req.Header, "X-Grafana-User") + }) + + t.Run("Should not apply user header when user is nil, should remove the existing", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + req.Header.Set("X-Grafana-User", "admin") + + ApplyUserHeader(false, req, nil) + require.NotContains(t, req.Header, "X-Grafana-User") + }) + + t.Run("Should not apply user header for anonomous user", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + + ApplyUserHeader(true, req, &user.SignedInUser{IsAnonymous: true}) + require.NotContains(t, req.Header, "X-Grafana-User") + }) + + t.Run("Should apply user header for non-anonomous user", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + + ApplyUserHeader(true, req, &user.SignedInUser{Login: "admin"}) + require.Equal(t, "admin", req.Header.Get("X-Grafana-User")) + }) +}