mirror of
https://github.com/grafana/grafana.git
synced 2025-01-19 21:13:35 -06:00
dataproxy: refactoring data source proxy to support route templates and wrote more tests for data proxy code, #9078
This commit is contained in:
parent
63d6ab476a
commit
8bf49c51b9
@ -32,8 +32,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||
handlers := make([]macaron.Handler, 0)
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
|
||||
ReqSignedIn: true,
|
||||
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
|
||||
ReqSignedIn: true,
|
||||
}))
|
||||
|
||||
if route.ReqRole != "" {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func getDatasource(id int64, orgId int64) (*m.DataSource, error) {
|
||||
@ -27,7 +28,14 @@ func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// find plugin
|
||||
plugin, ok := plugins.DataSources[ds.Type]
|
||||
if !ok {
|
||||
c.JsonApiErr(500, "Unable to find datasource plugin", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxyPath := c.Params("*")
|
||||
proxy := pluginproxy.NewDataSourceProxy(ds, c, proxyPath)
|
||||
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
|
||||
proxy.HandleRequest()
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestDataSourceProxy(t *testing.T) {
|
||||
Convey("When getting graphite datasource proxy", t, func() {
|
||||
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
|
||||
targetUrl, err := url.Parse(ds.Url)
|
||||
proxy := NewReverseProxy(&ds, "/render", targetUrl)
|
||||
proxy.Transport, err = ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
transport, ok := proxy.Transport.(*http.Transport)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.Director(&req)
|
||||
|
||||
Convey("Can translate request url and path", func() {
|
||||
So(req.URL.Host, ShouldEqual, "graphite:8080")
|
||||
So(req.URL.Path, ShouldEqual, "/render")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting influxdb datasource proxy", t, func() {
|
||||
ds := m.DataSource{
|
||||
Type: m.DS_INFLUXDB_08,
|
||||
Url: "http://influxdb:8083",
|
||||
Database: "site",
|
||||
User: "user",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
proxy := NewReverseProxy(&ds, "", targetUrl)
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.Director(&req)
|
||||
|
||||
Convey("Should add db to url", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/db/site/")
|
||||
})
|
||||
|
||||
Convey("Should add username and password", func() {
|
||||
queryVals := req.URL.Query()
|
||||
So(queryVals["u"][0], ShouldEqual, "user")
|
||||
So(queryVals["p"][0], ShouldEqual, "password")
|
||||
})
|
||||
})
|
||||
}
|
@ -32,13 +32,18 @@ type DataSourceProxy struct {
|
||||
targetUrl *url.URL
|
||||
proxyPath string
|
||||
route *plugins.AppPluginRoute
|
||||
plugin *plugins.DataSourcePlugin
|
||||
}
|
||||
|
||||
func NewDataSourceProxy(ds *m.DataSource, ctx *middleware.Context, proxyPath string) *DataSourceProxy {
|
||||
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *middleware.Context, proxyPath string) *DataSourceProxy {
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
|
||||
return &DataSourceProxy{
|
||||
ds: ds,
|
||||
plugin: plugin,
|
||||
ctx: ctx,
|
||||
proxyPath: proxyPath,
|
||||
targetUrl: targetUrl,
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +147,7 @@ func (proxy *DataSourceProxy) validateRequest() error {
|
||||
}
|
||||
}
|
||||
|
||||
targetUrl, _ := url.Parse(proxy.ds.Url)
|
||||
if !checkWhiteList(proxy.ctx, targetUrl.Host) {
|
||||
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
|
||||
return errors.New("Target url is not a valid target")
|
||||
}
|
||||
|
||||
@ -166,25 +170,26 @@ func (proxy *DataSourceProxy) validateRequest() error {
|
||||
}
|
||||
|
||||
// found route if there are any
|
||||
if plugin, ok := plugins.DataSources[proxy.ds.Type]; ok {
|
||||
if len(plugin.Routes) > 0 {
|
||||
for _, route := range plugin.Routes {
|
||||
// method match
|
||||
if route.Method != "*" && route.Method != proxy.ctx.Req.Method {
|
||||
continue
|
||||
}
|
||||
if len(proxy.plugin.Routes) > 0 {
|
||||
for _, route := range proxy.plugin.Routes {
|
||||
// method match
|
||||
if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(proxy.proxyPath, route.Path) {
|
||||
logger.Info("Apply Route Rule", "rule", route.Path)
|
||||
proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, route.Path)
|
||||
proxy.route = route
|
||||
break
|
||||
if route.ReqRole.IsValid() {
|
||||
if !proxy.ctx.HasUserRole(route.ReqRole) {
|
||||
return errors.New("Plugin proxy route access denied")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(proxy.proxyPath, route.Path) {
|
||||
proxy.route = route
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy.targetUrl = targetUrl
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -226,6 +231,8 @@ 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{
|
||||
JsonData: proxy.ds.JsonData.Interface().(map[string]interface{}),
|
||||
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
|
||||
|
@ -2,9 +2,13 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -14,51 +18,135 @@ import (
|
||||
|
||||
func TestDSRouteRule(t *testing.T) {
|
||||
|
||||
Convey("When applying ds route rule", t, func() {
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
Convey("DataSourceProxy", t, func() {
|
||||
Convey("Plugin with routes", func() {
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_EDITOR,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/admin",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_ADMIN,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/anon",
|
||||
Url: "https://www.google.com",
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
setting.SecretKey = "password"
|
||||
key, _ := util.Encrypt([]byte("123"), "password")
|
||||
setting.SecretKey = "password"
|
||||
key, _ := util.Encrypt([]byte("123"), "password")
|
||||
|
||||
ds := &m.DataSource{
|
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{
|
||||
"clientId": "asd",
|
||||
}),
|
||||
SecureJsonData: map[string][]byte{
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
ds := &m.DataSource{
|
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{
|
||||
"clientId": "asd",
|
||||
}),
|
||||
SecureJsonData: map[string][]byte{
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
|
||||
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
|
||||
ctx := &middleware.Context{
|
||||
Context: &macaron.Context{
|
||||
Req: macaron.Request{Request: req},
|
||||
},
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
|
||||
}
|
||||
|
||||
Convey("When not matching route path", func() {
|
||||
ApplyDataSourceRouteRules(req, plugin, ds, "/asdas/asd")
|
||||
Convey("When matching route path", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
|
||||
proxy.route = plugin.Routes[0]
|
||||
proxy.applyRoute(req)
|
||||
|
||||
Convey("should not touch req", func() {
|
||||
So(len(req.Header), ShouldEqual, 0)
|
||||
So(req.URL.String(), ShouldEqual, "http://localhost/asd")
|
||||
Convey("should add headers and update url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
|
||||
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Validating request", func() {
|
||||
Convey("plugin route with valid role", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("plugin route with admin role and user is editor", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("plugin route with admin role and user is admin", func() {
|
||||
ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When matching route path", func() {
|
||||
ApplyDataSourceRouteRules(req, plugin, ds, "api/v4/some/method")
|
||||
Convey("When proxying graphite", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
|
||||
ctx := &middleware.Context{}
|
||||
|
||||
Convey("should add headers and update url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
|
||||
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Can translate request url and path", func() {
|
||||
So(req.URL.Host, ShouldEqual, "graphite:8080")
|
||||
So(req.URL.Path, ShouldEqual, "/render")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying InfluxDB", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_INFLUXDB_08,
|
||||
Url: "http://influxdb:8083",
|
||||
Database: "site",
|
||||
User: "user",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
ctx := &middleware.Context{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should add db to url", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/db/site/")
|
||||
})
|
||||
|
||||
Convey("Should add username and password", func() {
|
||||
queryVals := req.URL.Query()
|
||||
So(queryVals["u"][0], ShouldEqual, "user")
|
||||
So(queryVals["p"][0], ShouldEqual, "password")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -23,12 +23,11 @@ type AppPlugin struct {
|
||||
}
|
||||
|
||||
type AppPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type AppPluginRouteHeader struct {
|
||||
|
Loading…
Reference in New Issue
Block a user