dsproxy: adds support for url params for plugin routes (#23503)
* dsproxy: adds support for url params for plugin routes * docs: fixes after review * pluginproxy: rename Params to URLParams * Update pkg/plugins/app_plugin.go Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Apply suggestions from code review Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * pluginproxy: rename struct Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
@@ -3,13 +3,14 @@ package pluginproxy
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -25,7 +26,7 @@ func TestAccessToken(t *testing.T) {
|
||||
Convey("Plugin with JWT token auth route", t, func() {
|
||||
pluginRoute := &plugins.AppPluginRoute{
|
||||
Path: "pathwithjwttoken1",
|
||||
Url: "https://api.jwt.io/some/path",
|
||||
URL: "https://api.jwt.io/some/path",
|
||||
Method: "GET",
|
||||
JwtTokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
@@ -108,7 +109,7 @@ func TestAccessToken(t *testing.T) {
|
||||
|
||||
pluginRoute := &plugins.AppPluginRoute{
|
||||
Path: "pathwithtokenauth1",
|
||||
Url: "",
|
||||
URL: "",
|
||||
Method: "GET",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: server.URL + "/oauth/token",
|
||||
|
||||
@@ -22,7 +22,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
SecureJsonData: ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := InterpolateString(route.Url, data)
|
||||
interpolatedURL, err := InterpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
@@ -39,6 +39,10 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
req.Host = routeURL.Host
|
||||
req.URL.Path = util.JoinURLFragments(routeURL.Path, proxyPath)
|
||||
|
||||
if err := addQueryString(req, route, data); err != nil {
|
||||
logger.Error("Failed to render plugin URL query string", "error", err)
|
||||
}
|
||||
|
||||
if err := addHeaders(&req.Header, route, data); err != nil {
|
||||
logger.Error("Failed to render plugin headers", "error", err)
|
||||
}
|
||||
@@ -79,6 +83,26 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func addQueryString(req *http.Request, route *plugins.AppPluginRoute, data templateData) error {
|
||||
q := req.URL.Query()
|
||||
for _, param := range route.URLParams {
|
||||
interpolatedName, err := InterpolateString(param.Name, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interpolatedContent, err := InterpolateString(param.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.Add(interpolatedName, interpolatedContent)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := InterpolateString(header.Content, data)
|
||||
|
||||
@@ -187,6 +187,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
} else {
|
||||
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
}
|
||||
|
||||
if proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.DecryptedBasicAuthPassword()))
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
@@ -44,7 +44,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "api/admin",
|
||||
Url: "https://www.google.com",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_ADMIN,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
@@ -52,14 +52,17 @@ func TestDSRouteRule(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "api/anon",
|
||||
Url: "https://www.google.com",
|
||||
URL: "https://www.google.com",
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/common",
|
||||
Url: "{{.JsonData.dynamicUrl}}",
|
||||
URL: "{{.JsonData.dynamicUrl}}",
|
||||
URLParams: []plugins.AppPluginRouteURLParam{
|
||||
{Name: "{{.JsonData.queryParam}}", Content: "{{.SecureJsonData.key}}"},
|
||||
},
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
@@ -74,6 +77,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{
|
||||
"clientId": "asd",
|
||||
"dynamicUrl": "https://dynamic.grafana.com",
|
||||
"queryParam": "apiKey",
|
||||
}),
|
||||
SecureJsonData: map[string][]byte{
|
||||
"key": key,
|
||||
@@ -106,8 +110,8 @@ func TestDSRouteRule(t *testing.T) {
|
||||
proxy.route = plugin.Routes[3]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
|
||||
Convey("should add headers and interpolate the url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
|
||||
Convey("should add headers and interpolate the url with query string parameters", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method?apiKey=123")
|
||||
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
})
|
||||
})
|
||||
@@ -142,7 +146,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "pathwithtoken1",
|
||||
Url: "https://api.nr1.io/some/path",
|
||||
URL: "https://api.nr1.io/some/path",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
@@ -155,7 +159,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "pathwithtoken2",
|
||||
Url: "https://api.nr2.io/some/path",
|
||||
URL: "https://api.nr2.io/some/path",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
|
||||
@@ -48,7 +48,7 @@ func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
interpolated, err := InterpolateString(route.Url, data)
|
||||
interpolated, err := InterpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string
|
||||
|
||||
// NewApiPluginProxy create a plugin proxy
|
||||
func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
|
||||
targetURL, _ := url.Parse(route.Url)
|
||||
targetURL, _ := url.Parse(route.URL)
|
||||
|
||||
director := func(req *http.Request) {
|
||||
|
||||
@@ -98,7 +98,7 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
|
||||
}
|
||||
}
|
||||
|
||||
if len(route.Url) > 0 {
|
||||
if len(route.URL) > 0 {
|
||||
interpolatedURL, err := updateURL(route, ctx.OrgId, appID)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
|
||||
Convey("When getting templated url", t, func() {
|
||||
route := &plugins.AppPluginRoute{
|
||||
Url: "{{.JsonData.dynamicUrl}}",
|
||||
URL: "{{.JsonData.dynamicUrl}}",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com")
|
||||
})
|
||||
Convey("Route url should not be modified", func() {
|
||||
So(route.Url, ShouldEqual, "{{.JsonData.dynamicUrl}}")
|
||||
So(route.URL, ShouldEqual, "{{.JsonData.dynamicUrl}}")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -138,13 +138,13 @@ func getPluginProxiedRequest(ctx *models.ReqContext, cfg *setting.Cfg, route *pl
|
||||
if route == nil {
|
||||
route = &plugins.AppPluginRoute{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
}
|
||||
}
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, route.Url, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, route.URL, nil)
|
||||
So(err, ShouldBeNil)
|
||||
proxy.Director(req)
|
||||
return req
|
||||
|
||||
Reference in New Issue
Block a user