mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
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:
parent
027e886997
commit
aad43869c3
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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.",
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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}
|
||||||
|
@ -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.
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user