diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 41ce857db4f..7abb5da1cec 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -217,7 +217,10 @@ func (this *thunderTask) Fetch() { this.Done() } -var client = &http.Client{} +var client *http.Client = &http.Client{ + Timeout: time.Second * 2, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, +} func (this *thunderTask) fetch() error { this.Avatar.timestamp = time.Now() diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 11215a6eaa3..c276c9155c7 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -2,6 +2,7 @@ package pluginproxy import ( "bytes" + "encoding/json" "errors" "fmt" "html/template" @@ -10,6 +11,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strconv" "strings" "time" @@ -23,9 +25,19 @@ import ( ) var ( - logger log.Logger = log.New("data-proxy-log") + logger log.Logger = log.New("data-proxy-log") + client *http.Client = &http.Client{ + Timeout: time.Second * 30, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + } ) +type jwtToken struct { + ExpiresOn time.Time `json:"-"` + ExpiresOnString string `json:"expires_on"` + AccessToken string `json:"access_token"` +} + type DataSourceProxy struct { ds *m.DataSource ctx *middleware.Context @@ -229,8 +241,6 @@ func checkWhiteList(c *middleware.Context, host string) bool { } func (proxy *DataSourceProxy) applyRoute(req *http.Request) { - logger.Info("ApplyDataSourceRouteRules", "route", proxy.route.Path, "proxyPath", proxy.proxyPath) - proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path) data := templateData{ @@ -238,8 +248,6 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) { SecureJsonData: proxy.ds.SecureJsonData.Decrypt(), } - logger.Info("Apply Route Rule", "rule", proxy.route.Path) - routeUrl, err := url.Parse(proxy.route.Url) if err != nil { logger.Error("Error parsing plugin route url") @@ -254,25 +262,80 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) { if err := addHeaders(&req.Header, proxy.route, data); err != nil { logger.Error("Failed to render plugin headers", "error", err) } -} -func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error { - for _, header := range route.Headers { - var contentBuf bytes.Buffer - t, err := template.New("content").Parse(header.Content) - if err != nil { - return errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name)) + if proxy.route.TokenAuth != nil { + if token, err := proxy.getAccessToken(data); err != nil { + logger.Error("Failed to get access token", "error", err) + } else { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) } + } +} - err = t.Execute(&contentBuf, data) - if err != nil { - return errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name)) +func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) { + urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data) + if err != nil { + return "", err + } + + logger.Info("client secret", "ClientSecret", data.SecureJsonData["clientSecret"]) + params := make(url.Values) + for key, value := range proxy.route.TokenAuth.Params { + if interpolatedParam, err := interpolateString(value, data); err != nil { + return "", err + } else { + logger.Info("param", key, interpolatedParam) + params.Add(key, interpolatedParam) } + } - value := contentBuf.String() + getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode())) + getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") + getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode()))) - logger.Info("Adding headers", "name", header.Name, "value", value) - reqHeaders.Add(header.Name, value) + resp, err := client.Do(getTokenReq) + if err != nil { + return "", err + } + + defer resp.Body.Close() + respData, err := ioutil.ReadAll(resp.Body) + logger.Info("Resp", "resp", string(respData)) + + var token jwtToken + if err := json.NewDecoder(resp.Body).Decode(&token); err != nil { + return "", err + } + + expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64) + token.ExpiresOn = time.Unix(expiresOnEpoch, 0) + + logger.Debug("Got new access token", "ExpiresOn", token.ExpiresOn) + return "", nil +} + +func interpolateString(text string, data templateData) (string, error) { + t, err := template.New("content").Parse(text) + if err != nil { + return "", errors.New(fmt.Sprintf("Could not parse template %s.", text)) + } + + var contentBuf bytes.Buffer + err = t.Execute(&contentBuf, data) + if err != nil { + return "", errors.New(fmt.Sprintf("Failed to execute template %s.", text)) + } + + return contentBuf.String(), 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 diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 831c4e2a3e4..d00b45fdb86 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -148,5 +148,18 @@ func TestDSRouteRule(t *testing.T) { So(queryVals["p"][0], ShouldEqual, "password") }) }) + + Convey("When interpolating string", func() { + data := templateData{ + SecureJsonData: map[string]string{ + "Test": "0+0a0sdasd00+++", + }, + } + + interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data) + So(err, ShouldBeNil) + So(interpolated, ShouldEqual, "0+0a0sdasd00+++") + }) + }) } diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 878d91f11f4..b070ba592f0 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -23,11 +23,12 @@ type AppPlugin struct { } type AppPluginRoute struct { - Path string `json:"path"` - Method string `json:"method"` - ReqRole models.RoleType `json:"reqRole"` - Url string `json:"url"` - Headers []AppPluginRouteHeader `json:"headers"` + 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"` } type AppPluginRouteHeader struct { @@ -35,6 +36,11 @@ type AppPluginRouteHeader struct { Content string `json:"content"` } +type JwtTokenAuth struct { + Url string `json:"url"` + Params map[string]string `json:"params"` +} + func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error { if err := decoder.Decode(&app); err != nil { return err diff --git a/tests/datasource-test/module.js b/tests/datasource-test/module.js index 83db563abb9..1937e20aa9c 100644 --- a/tests/datasource-test/module.js +++ b/tests/datasource-test/module.js @@ -8,12 +8,20 @@ System.register([], function (_export) { function Datasource(instanceSettings, backendSrv) { this.url = instanceSettings.url; + // this.testDatasource = function() { + // return backendSrv.datasourceRequest({ + // method: 'GET', + // url: this.url + '/api/v4/search' + // }); + // } + // this.testDatasource = function() { return backendSrv.datasourceRequest({ method: 'GET', - url: this.url + '/api/v4/search' + url: this.url + '/tokenTest' }); } + } function ConfigCtrl() { @@ -22,12 +30,16 @@ System.register([], function (_export) { ConfigCtrl.template = `
- - + + +
+
+ +
- - + +
`; diff --git a/tests/datasource-test/plugin.json b/tests/datasource-test/plugin.json index a55be2ef800..94c57424413 100644 --- a/tests/datasource-test/plugin.json +++ b/tests/datasource-test/plugin.json @@ -5,12 +5,12 @@ "routes": [ { - "path": "api/v5/", + "path": "tokenTest", "method": "*", - "url": "https://grafana-api.kentik.com/api/v5", + "url": "http://localhost:3333/query", "tokenAuth": { - "url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token", - "body": { + "url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token", + "params": { "grant_type": "client_credentials", "client_id": "{{.JsonData.clientId}}", "client_secret": "{{.SecureJsonData.clientSecret}}",