datasource-proxy: token exchange

This commit is contained in:
Torkel Ödegaard 2017-08-23 17:18:43 +02:00
parent 4f9fbcc211
commit 3c9798bec9
6 changed files with 130 additions and 33 deletions

View File

@ -217,7 +217,10 @@ func (this *thunderTask) Fetch() {
this.Done() 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 { func (this *thunderTask) fetch() error {
this.Avatar.timestamp = time.Now() this.Avatar.timestamp = time.Now()

View File

@ -2,6 +2,7 @@ package pluginproxy
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@ -10,6 +11,7 @@ import (
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@ -23,9 +25,19 @@ import (
) )
var ( 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 { type DataSourceProxy struct {
ds *m.DataSource ds *m.DataSource
ctx *middleware.Context ctx *middleware.Context
@ -229,8 +241,6 @@ func checkWhiteList(c *middleware.Context, host string) bool {
} }
func (proxy *DataSourceProxy) applyRoute(req *http.Request) { 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) proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
data := templateData{ data := templateData{
@ -238,8 +248,6 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(), SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
} }
logger.Info("Apply Route Rule", "rule", proxy.route.Path)
routeUrl, err := url.Parse(proxy.route.Url) routeUrl, err := url.Parse(proxy.route.Url)
if err != nil { if err != nil {
logger.Error("Error parsing plugin route url") 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 { if err := addHeaders(&req.Header, proxy.route, data); err != nil {
logger.Error("Failed to render plugin headers", "error", err) logger.Error("Failed to render plugin headers", "error", err)
} }
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))
}
}
}
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)
}
}
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())))
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 { func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
for _, header := range route.Headers { for _, header := range route.Headers {
var contentBuf bytes.Buffer interpolated, err := interpolateString(header.Content, data)
t, err := template.New("content").Parse(header.Content)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name)) return err
} }
reqHeaders.Add(header.Name, interpolated)
err = t.Execute(&contentBuf, data)
if err != nil {
return errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name))
}
value := contentBuf.String()
logger.Info("Adding headers", "name", header.Name, "value", value)
reqHeaders.Add(header.Name, value)
} }
return nil return nil

View File

@ -148,5 +148,18 @@ func TestDSRouteRule(t *testing.T) {
So(queryVals["p"][0], ShouldEqual, "password") 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+++")
})
}) })
} }

View File

@ -23,11 +23,12 @@ type AppPlugin struct {
} }
type AppPluginRoute struct { type AppPluginRoute struct {
Path string `json:"path"` Path string `json:"path"`
Method string `json:"method"` Method string `json:"method"`
ReqRole models.RoleType `json:"reqRole"` ReqRole models.RoleType `json:"reqRole"`
Url string `json:"url"` Url string `json:"url"`
Headers []AppPluginRouteHeader `json:"headers"` Headers []AppPluginRouteHeader `json:"headers"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
} }
type AppPluginRouteHeader struct { type AppPluginRouteHeader struct {
@ -35,6 +36,11 @@ type AppPluginRouteHeader struct {
Content string `json:"content"` 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 { func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&app); err != nil { if err := decoder.Decode(&app); err != nil {
return err return err

View File

@ -8,12 +8,20 @@ System.register([], function (_export) {
function Datasource(instanceSettings, backendSrv) { function Datasource(instanceSettings, backendSrv) {
this.url = instanceSettings.url; this.url = instanceSettings.url;
// this.testDatasource = function() {
// return backendSrv.datasourceRequest({
// method: 'GET',
// url: this.url + '/api/v4/search'
// });
// }
//
this.testDatasource = function() { this.testDatasource = function() {
return backendSrv.datasourceRequest({ return backendSrv.datasourceRequest({
method: 'GET', method: 'GET',
url: this.url + '/api/v4/search' url: this.url + '/tokenTest'
}); });
} }
} }
function ConfigCtrl() { function ConfigCtrl() {
@ -22,12 +30,16 @@ System.register([], function (_export) {
ConfigCtrl.template = ` ConfigCtrl.template = `
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-13">Email </label> <label class="gf-form-label width-13">TenantId </label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.email'></input> <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.tenantId'></input>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-13">Access key ID </label> <label class="gf-form-label width-13">ClientId </label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.token'></input> <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">Client secret</label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.clientSecret'></input>
</div> </div>
`; `;

View File

@ -5,12 +5,12 @@
"routes": [ "routes": [
{ {
"path": "api/v5/", "path": "tokenTest",
"method": "*", "method": "*",
"url": "https://grafana-api.kentik.com/api/v5", "url": "http://localhost:3333/query",
"tokenAuth": { "tokenAuth": {
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token", "url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
"body": { "params": {
"grant_type": "client_credentials", "grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}", "client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}", "client_secret": "{{.SecureJsonData.clientSecret}}",