mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
datasource-proxy: token exchange
This commit is contained in:
parent
4f9fbcc211
commit
3c9798bec9
@ -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()
|
||||||
|
@ -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"
|
||||||
|
|
||||||
@ -24,8 +26,18 @@ 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
|
||||||
|
@ -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+++")
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ type AppPluginRoute struct {
|
|||||||
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
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -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}}",
|
||||||
|
Loading…
Reference in New Issue
Block a user