Commit 52154b46 by Daniel Lee Committed by GitHub

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>
parent 59bea141
......@@ -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
......
......@@ -23,21 +23,33 @@ type AppPlugin struct {
Pinned bool `json:"-"`
}
// AppPluginRoute describes a plugin route that is defined in
// the plugin.json file for a plugin.
type AppPluginRoute struct {
Path string `json:"path"`
Method string `json:"method"`
ReqRole models.RoleType `json:"reqRole"`
Url string `json:"url"`
Headers []AppPluginRouteHeader `json:"headers"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
Path string `json:"path"`
Method string `json:"method"`
ReqRole models.RoleType `json:"reqRole"`
URL string `json:"url"`
URLParams []AppPluginRouteURLParam `json:"urlParams"`
Headers []AppPluginRouteHeader `json:"headers"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
}
// AppPluginRouteHeader describes an HTTP header that is forwarded with
// the proxied request for a plugin route
type AppPluginRouteHeader struct {
Name string `json:"name"`
Content string `json:"content"`
}
// AppPluginRouteURLParam describes query string parameters for
// a url in a plugin route
type AppPluginRouteURLParam struct {
Name string `json:"name"`
Content string `json:"content"`
}
// JwtTokenAuth struct is both for normal Token Auth and JWT Token Auth with
// an uploaded JWT file.
type JwtTokenAuth struct {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment