Browse Source

datasource-proxy: token exchange

Torkel Ödegaard 8 years ago
parent
commit
3c9798bec9

+ 4 - 1
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()

+ 81 - 18
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

+ 13 - 0
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+++")
+		})
+
 	})
 }

+ 11 - 5
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

+ 17 - 5
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 = `
         <div class="gf-form">
-          <label class="gf-form-label width-13">Email </label>
-          <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.email'></input>
+          <label class="gf-form-label width-13">TenantId </label>
+          <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.tenantId'></input>
+         </div>
+         <div class="gf-form">
+          <label class="gf-form-label width-13">ClientId </label>
+          <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.clientId'></input>
          </div>
          <div class="gf-form">
-          <label class="gf-form-label width-13">Access key ID </label>
-          <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.token'></input>
+          <label class="gf-form-label width-13">Client secret</label>
+          <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.clientSecret'></input>
          </div>
       `;
 

+ 4 - 4
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}}",