Plugins: Support set body content in plugin routes (#32551)

Adds support for overriding the body and length in plugin routes.
This commit is contained in:
Marcus Efraimsson 2021-03-31 16:38:35 +02:00 committed by GitHub
parent 027e886997
commit aad43869c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 0 deletions

View File

@ -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 ### Enable token authentication
To enable token-based authentication for proxied requests, use the `tokenAuth` property. To enable token-based authentication for proxied requests, use the `tokenAuth` property.

View File

@ -174,6 +174,7 @@ For data source plugins. Proxy routes used for plugin authentication and adding
| Property | Type | Required | Description | | 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. | | `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. | | `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. | | `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. | | `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. | | `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 ### jwtTokenAuth
For data source plugins. Token authentication section used with an JWT OAuth API. For data source plugins. Token authentication section used with an JWT OAuth API.

View File

@ -350,6 +350,10 @@
"type": "array", "type": "array",
"description": "For data source plugins. Route headers adds HTTP headers to the proxied request." "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": { "tokenAuth": {
"type": "object", "type": "object",
"description": "For data source plugins. Token authentication section used with an OAuth API.", "description": "For data source plugins. Token authentication section used with an OAuth API.",

View File

@ -50,6 +50,10 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
logger.Error("Failed to render plugin headers", "error", err) 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) tokenProvider := newAccessTokenProvider(ds, route)
if route.TokenAuth != nil { if route.TokenAuth != nil {

View File

@ -69,6 +69,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
Path: "api/restricted", Path: "api/restricted",
ReqRole: models.ROLE_ADMIN, 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()) 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("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) { t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp() ctx, _ := setUp()

View File

@ -70,6 +70,10 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
ctx.JsonApiErr(500, "Failed to render plugin headers", err) ctx.JsonApiErr(500, "Failed to render plugin headers", err)
return 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} return &httputil.ReverseProxy{Director: director}

View File

@ -1,10 +1,12 @@
package pluginproxy package pluginproxy
import ( import (
"io/ioutil"
"net/http" "net/http"
"testing" "testing"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -148,6 +150,38 @@ func TestPluginProxy(t *testing.T) {
) )
assert.Equal(t, "https://example.com", req.URL.String()) 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. // getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.

View File

@ -3,7 +3,9 @@ package pluginproxy
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"strings"
"text/template" "text/template"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
@ -60,6 +62,20 @@ func addQueryString(req *http.Request, route *plugins.AppPluginRoute, data templ
return nil 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) // Set the X-Grafana-User header if needed (and remove if not)
func applyUserHeader(sendUserHeader bool, req *http.Request, user *models.SignedInUser) { func applyUserHeader(sendUserHeader bool, req *http.Request, user *models.SignedInUser) {
req.Header.Del("X-Grafana-User") req.Header.Del("X-Grafana-User")

View File

@ -35,6 +35,7 @@ type AppPluginRoute struct {
Headers []AppPluginRouteHeader `json:"headers"` Headers []AppPluginRouteHeader `json:"headers"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"` TokenAuth *JwtTokenAuth `json:"tokenAuth"`
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"` JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
Body json.RawMessage `json:"body"`
} }
// AppPluginRouteHeader describes an HTTP header that is forwarded with // AppPluginRouteHeader describes an HTTP header that is forwarded with