From aad43869c32695ed9558c736b3abf4fe7a4d8d7e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Mar 2021 16:38:35 +0200 Subject: [PATCH] Plugins: Support set body content in plugin routes (#32551) Adds support for overriding the body and length in plugin routes. --- ...-authentication-for-data-source-plugins.md | 17 ++++++++++ docs/sources/developers/plugins/metadata.md | 8 +++++ .../developers/plugins/plugin.schema.json | 4 +++ pkg/api/pluginproxy/ds_auth_provider.go | 4 +++ pkg/api/pluginproxy/ds_proxy_test.go | 17 ++++++++++ pkg/api/pluginproxy/pluginproxy.go | 4 +++ pkg/api/pluginproxy/pluginproxy_test.go | 34 +++++++++++++++++++ pkg/api/pluginproxy/utils.go | 16 +++++++++ pkg/plugins/app_plugin.go | 1 + 9 files changed, 105 insertions(+) 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 36bcccef666..5ab1da89521 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 @@ -131,6 +131,23 @@ To add URL parameters to proxied requests, use the `urlParams` property. ] ``` +### Set body content + +To set the body content and length of proxied requests, use the `body` property. + +```json +"routes": [ + { + "path": "example", + "url": "http://api.example.com", + "body": { + "username": "{{ .JsonData.username }}", + "password": "{{ .SecureJsonData.password }}" + } + } +] +``` + ### Enable token authentication To enable token-based authentication for proxied requests, use the `tokenAuth` property. diff --git a/docs/sources/developers/plugins/metadata.md b/docs/sources/developers/plugins/metadata.md index eb2c1003275..06c1ee549d6 100644 --- a/docs/sources/developers/plugins/metadata.md +++ b/docs/sources/developers/plugins/metadata.md @@ -174,6 +174,7 @@ For data source plugins. Proxy routes used for plugin authentication and adding | Property | Type | Required | Description | |----------------|-------------------------|----------|---------------------------------------------------------------------------------------------------------| +| `body` | [object](#body) | No | For data source plugins. Route headers set the body content and length to the proxied request. | | `headers` | array | No | For data source plugins. Route headers adds HTTP headers to the proxied request. | | `jwtTokenAuth` | [object](#jwttokenauth) | No | For data source plugins. Token authentication section used with an JWT OAuth API. | | `method` | string | No | For data source plugins. Route method matches the HTTP verb like GET or POST. | @@ -183,6 +184,13 @@ For data source plugins. Proxy routes used for plugin authentication and adding | `tokenAuth` | [object](#tokenauth) | No | For data source plugins. Token authentication section used with an OAuth API. | | `url` | string | No | For data source plugins. Route URL is where the request is proxied to. | +### body + +For data source plugins. Route headers set the body content and length to the proxied request. + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + ### jwtTokenAuth For data source plugins. Token authentication section used with an JWT OAuth API. diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index c12b1960b2f..906ddadcd0b 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -350,6 +350,10 @@ "type": "array", "description": "For data source plugins. Route headers adds HTTP headers to the proxied request." }, + "body": { + "type": "object", + "description": "For data source plugins. Route headers set the body content and length to the proxied request." + }, "tokenAuth": { "type": "object", "description": "For data source plugins. Token authentication section used with an OAuth API.", diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 493d2e997d1..128137e4b8b 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -50,6 +50,10 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route logger.Error("Failed to render plugin headers", "error", err) } + if err := setBodyContent(req, route, data); err != nil { + logger.Error("Failed to set plugin route body content", "error", err) + } + tokenProvider := newAccessTokenProvider(ds, route) if route.TokenAuth != nil { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 07dbd1d7074..0f51cd2d7a8 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -69,6 +69,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) { Path: "api/restricted", ReqRole: models.ROLE_ADMIN, }, + { + Path: "api/body", + URL: "http://www.test.com", + Body: []byte(`{ "url": "{{.JsonData.dynamicUrl}}", "secret": "{{.SecureJsonData.key}}" }`), + }, }, } @@ -136,6 +141,18 @@ func TestDataSourceProxy_routeRule(t *testing.T) { assert.Equal(t, "http://localhost/asd", req.URL.String()) }) + t.Run("When matching route path and has dynamic body", func(t *testing.T) { + ctx, req := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/body", &setting.Cfg{}) + require.NoError(t, err) + proxy.route = plugin.Routes[5] + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) + + content, err := ioutil.ReadAll(req.Body) + require.NoError(t, err) + require.Equal(t, `{ "url": "https://dynamic.grafana.com", "secret": "123" }`, string(content)) + }) + t.Run("Validating request", func(t *testing.T) { t.Run("plugin route with valid role", func(t *testing.T) { ctx, _ := setUp() diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index d145480afc2..7ca8f662a85 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -70,6 +70,10 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins. ctx.JsonApiErr(500, "Failed to render plugin headers", err) return } + + if err := setBodyContent(req, route, data); err != nil { + logger.Error("Failed to set plugin route body content", "error", err) + } } return &httputil.ReverseProxy{Director: director} diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index 7b538139bbc..9f8c12a0903 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -1,10 +1,12 @@ package pluginproxy import ( + "io/ioutil" "net/http" "testing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" @@ -148,6 +150,38 @@ func TestPluginProxy(t *testing.T) { ) assert.Equal(t, "https://example.com", req.URL.String()) }) + + t.Run("When getting templated body", func(t *testing.T) { + route := &plugins.AppPluginRoute{ + Path: "api/body", + URL: "http://www.test.com", + Body: []byte(`{ "url": "{{.JsonData.dynamicUrl}}", "secret": "{{.SecureJsonData.key}}" }`), + } + + bus.AddHandler("test", func(query *models.GetPluginSettingByIdQuery) error { + query.Result = &models.PluginSetting{ + JsonData: map[string]interface{}{ + "dynamicUrl": "https://dynamic.grafana.com", + }, + SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{"key": "123"}), + } + return nil + }) + + req := getPluginProxiedRequest( + t, + &models.ReqContext{ + SignedInUser: &models.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: true}, + route, + ) + content, err := ioutil.ReadAll(req.Body) + require.NoError(t, err) + require.Equal(t, `{ "url": "https://dynamic.grafana.com", "secret": "123" }`, string(content)) + }) } // getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. diff --git a/pkg/api/pluginproxy/utils.go b/pkg/api/pluginproxy/utils.go index b1bc6d1c912..a2f10dca434 100644 --- a/pkg/api/pluginproxy/utils.go +++ b/pkg/api/pluginproxy/utils.go @@ -3,7 +3,9 @@ package pluginproxy import ( "bytes" "fmt" + "io/ioutil" "net/http" + "strings" "text/template" "github.com/grafana/grafana/pkg/models" @@ -60,6 +62,20 @@ func addQueryString(req *http.Request, route *plugins.AppPluginRoute, data templ return nil } +func setBodyContent(req *http.Request, route *plugins.AppPluginRoute, data templateData) error { + if route.Body != nil { + interpolatedBody, err := interpolateString(string(route.Body), data) + if err != nil { + return err + } + + req.Body = ioutil.NopCloser(strings.NewReader(interpolatedBody)) + req.ContentLength = int64(len(interpolatedBody)) + } + + return nil +} + // Set the X-Grafana-User header if needed (and remove if not) func applyUserHeader(sendUserHeader bool, req *http.Request, user *models.SignedInUser) { req.Header.Del("X-Grafana-User") diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 5c159b0577b..53f76b7fc5c 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -35,6 +35,7 @@ type AppPluginRoute struct { Headers []AppPluginRouteHeader `json:"headers"` TokenAuth *JwtTokenAuth `json:"tokenAuth"` JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"` + Body json.RawMessage `json:"body"` } // AppPluginRouteHeader describes an HTTP header that is forwarded with