mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Fix panic when using complex dynamic URLs in app plugin routes (#27977)
* remove unused function to interpolate URLs * share function to add headers between ds/plugin proxies * stop performing unnecessary plugin setting lookup * fix bug causing runtime errors when using complex templated URLs * lower case util functions not used outside of pluginproxy package * change test URL to a (valid) dummy URL to make intent clearer Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
0f3bebb38d
commit
967e9b39e8
@ -57,12 +57,7 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appID string, hs *HTTPServer)
|
||||
return func(c *models.ReqContext) {
|
||||
path := c.Params("*")
|
||||
|
||||
proxy, err := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to create API plugin proxy", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg)
|
||||
proxy.Transport = pluginProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
}
|
||||
|
@ -99,14 +99,14 @@ func (provider *accessTokenProvider) getAccessToken(data templateData) (string,
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := InterpolateString(provider.route.TokenAuth.Url, data)
|
||||
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range provider.route.TokenAuth.Params {
|
||||
interpolatedParam, err := InterpolateString(value, data)
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -147,7 +147,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
conf := &jwt.Config{}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -155,7 +155,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -163,7 +163,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
}
|
||||
|
||||
if len(route.URL) > 0 {
|
||||
interpolatedURL, err := InterpolateString(route.URL, data)
|
||||
interpolatedURL, err := interpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
@ -84,35 +84,3 @@ 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqHeaders.Add(header.Name, interpolated)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ func TestDsAuthProvider(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -20,55 +19,35 @@ type templateData struct {
|
||||
SecureJsonData map[string]string
|
||||
}
|
||||
|
||||
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.Header, error) {
|
||||
result := http.Header{}
|
||||
|
||||
query := models.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
err := addHeaders(&result, route, data)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string, error) {
|
||||
query := models.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
interpolated, err := InterpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return interpolated, err
|
||||
}
|
||||
|
||||
// NewApiPluginProxy create a plugin proxy
|
||||
func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string,
|
||||
cfg *setting.Cfg) (*httputil.ReverseProxy, error) {
|
||||
targetURL, err := url.Parse(route.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
query := models.GetPluginSettingByIdQuery{OrgId: ctx.OrgId, PluginId: appID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.JsonApiErr(500, "Failed to fetch plugin settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := interpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
return
|
||||
}
|
||||
targetURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not parse url", err)
|
||||
return
|
||||
}
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
req.URL.Host = targetURL.Host
|
||||
req.Host = targetURL.Host
|
||||
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
@ -86,38 +65,11 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
|
||||
|
||||
applyUserHeader(cfg.SendUserHeader, req, ctx.SignedInUser)
|
||||
|
||||
if len(route.Headers) > 0 {
|
||||
headers, err := getHeaders(route, ctx.OrgId, appID)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not generate plugin route header", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
log.Tracef("setting key %v value <redacted>", key)
|
||||
req.Header.Set(key, value[0])
|
||||
}
|
||||
if err := addHeaders(&req.Header, route, data); err != nil {
|
||||
ctx.JsonApiErr(500, "Failed to render plugin headers", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
targetURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not parse custom url: %v", err)
|
||||
return
|
||||
}
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
req.URL.Host = targetURL.Host
|
||||
req.Host = targetURL.Host
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
}
|
||||
|
||||
// reqBytes, _ := httputil.DumpRequestOut(req, true);
|
||||
// log.Tracef("Proxying plugin request: %s", string(reqBytes))
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}, nil
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
@ -36,11 +36,18 @@ func TestPluginProxy(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
header, err := getHeaders(route, 1, "my-app")
|
||||
So(err, ShouldBeNil)
|
||||
req := getPluginProxiedRequest(
|
||||
&models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{
|
||||
Login: "test_user",
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
route,
|
||||
)
|
||||
|
||||
Convey("Should render header template", func() {
|
||||
So(header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
})
|
||||
})
|
||||
|
||||
@ -116,11 +123,6 @@ func TestPluginProxy(t *testing.T) {
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
route,
|
||||
)
|
||||
Convey("Headers should be updated", func() {
|
||||
header, err := getHeaders(route, 1, "my-app")
|
||||
So(err, ShouldBeNil)
|
||||
So(header.Get("X-Grafana-User"), ShouldEqual, "")
|
||||
})
|
||||
Convey("Should set req.URL to be interpolated value from jsonData", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com")
|
||||
})
|
||||
@ -128,6 +130,31 @@ func TestPluginProxy(t *testing.T) {
|
||||
So(route.URL, ShouldEqual, "{{.JsonData.dynamicUrl}}")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting complex templated url", t, func() {
|
||||
route := &plugins.AppPluginRoute{
|
||||
URL: "{{if .JsonData.apiHost}}{{.JsonData.apiHost}}{{else}}https://example.com{{end}}",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetPluginSettingByIdQuery) error {
|
||||
query.Result = &models.PluginSetting{}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := getPluginProxiedRequest(
|
||||
&models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{
|
||||
Login: "test_user",
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
route,
|
||||
)
|
||||
Convey("Should set req.URL to be interpolated default value", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://example.com")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
@ -140,10 +167,9 @@ func getPluginProxiedRequest(ctx *models.ReqContext, cfg *setting.Cfg, route *pl
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
}
|
||||
}
|
||||
proxy, err := NewApiPluginProxy(ctx, "", route, "", cfg)
|
||||
So(err, ShouldBeNil)
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, route.URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/plugin-proxy/grafana-simple-app/api/v4/alerts", nil)
|
||||
So(err, ShouldBeNil)
|
||||
proxy.Director(req)
|
||||
return req
|
||||
|
@ -4,15 +4,14 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
// InterpolateString accepts template data and return a string with substitutions
|
||||
func InterpolateString(text string, data templateData) (string, error) {
|
||||
// interpolateString accepts template data and return a string with substitutions
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
@ -27,26 +26,38 @@ func InterpolateString(text string, data templateData) (string, error) {
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
// InterpolateURL accepts template data and return a string with substitutions
|
||||
func InterpolateURL(anURL *url.URL, route *plugins.AppPluginRoute, orgID int64, appID string) (*url.URL, error) {
|
||||
query := models.GetPluginSettingByIdQuery{OrgId: orgID, PluginId: appID}
|
||||
result, err := url.Parse(anURL.String())
|
||||
if query.Result != nil {
|
||||
if len(query.Result.JsonData) > 0 {
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
}
|
||||
interpolatedResult, err := InterpolateString(anURL.String(), data)
|
||||
if err == nil {
|
||||
result, err = url.Parse(interpolatedResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing plugin route URL: %w", err)
|
||||
}
|
||||
}
|
||||
// addHeaders interpolates route headers and injects them into the request headers
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqHeaders.Set(header.Name, interpolated)
|
||||
}
|
||||
|
||||
return result, err
|
||||
return nil
|
||||
}
|
||||
|
||||
// addQueryString interpolates route params and injects them into the request object
|
||||
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
|
||||
}
|
||||
|
||||
// Set the X-Grafana-User header if needed (and remove if not)
|
||||
|
@ -14,7 +14,7 @@ func TestInterpolateString(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user