From 37c6a1ddf09a893844b93ef01e546f681e1ee176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 10 Feb 2016 16:43:35 +0100 Subject: [PATCH] feat(app routes): worked on app routes, added unit test, changed Grafana-Context header to start with X to be standard compliant, got cloud saas queries to work via app route feature and header template --- pkg/api/app_routes.go | 90 +++------------------- pkg/api/pluginproxy/pluginproxy.go | 99 +++++++++++++++++++++++++ pkg/api/pluginproxy/pluginproxy_test.go | 42 +++++++++++ pkg/models/app_settings.go | 8 ++ pkg/plugins/app_plugin.go | 1 - pkg/services/sqlstore/app_settings.go | 8 +- 6 files changed, 161 insertions(+), 87 deletions(-) create mode 100644 pkg/api/pluginproxy/pluginproxy.go create mode 100644 pkg/api/pluginproxy/pluginproxy_test.go diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 169c5c6d15c..5796f09bb21 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -1,17 +1,9 @@ package api import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httputil" - "net/url" - "text/template" - "gopkg.in/macaron.v1" - "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -22,16 +14,14 @@ import ( func InitAppPluginRoutes(r *macaron.Macaron) { for _, plugin := range plugins.Apps { for _, route := range plugin.Routes { - log.Info("Plugin: Adding proxy route for app plugin") - url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path) + url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path) handlers := make([]macaron.Handler, 0) - if route.ReqSignedIn { - handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})) - } - if route.ReqGrafanaAdmin { - handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})) - } - if route.ReqSignedIn && route.ReqRole != "" { + handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ + ReqSignedIn: true, + ReqGrafanaAdmin: route.ReqGrafanaAdmin, + })) + + if route.ReqRole != "" { if route.ReqRole == m.ROLE_ADMIN { handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN)) } else if route.ReqRole == m.ROLE_EDITOR { @@ -40,7 +30,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) { } handlers = append(handlers, AppPluginRoute(route, plugin.Id)) r.Route(url, route.Method, handlers...) - log.Info("Plugin: Adding route %s", url) + log.Info("Plugins: Adding proxy route %s", url) } } } @@ -49,68 +39,8 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler return func(c *middleware.Context) { path := c.Params("*") - proxy := NewApiPluginProxy(c, path, route, appId) + proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) proxy.Transport = dataProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } } - -func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy { - targetUrl, _ := url.Parse(route.Url) - - director := func(req *http.Request) { - - req.URL.Scheme = targetUrl.Scheme - req.URL.Host = targetUrl.Host - req.Host = targetUrl.Host - - req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath) - - // clear cookie headers - req.Header.Del("Cookie") - req.Header.Del("Set-Cookie") - - //Create a HTTP header with the context in it. - ctxJson, err := json.Marshal(ctx.SignedInUser) - if err != nil { - ctx.JsonApiErr(500, "failed to marshal context to json.", err) - return - } - - req.Header.Add("Grafana-Context", string(ctxJson)) - // add custom headers defined in the plugin config. - for _, header := range route.Headers { - var contentBuf bytes.Buffer - t, err := template.New("content").Parse(header.Content) - if err != nil { - ctx.JsonApiErr(500, fmt.Sprintf("could not parse header content template for header %s.", header.Name), err) - return - } - - //lookup appSettings - query := m.GetAppSettingByAppIdQuery{OrgId: ctx.OrgId, AppId: appId} - - if err := bus.Dispatch(&query); err != nil { - ctx.JsonApiErr(500, "failed to get AppSettings.", err) - return - } - type templateData struct { - JsonData map[string]interface{} - SecureJsonData map[string]string - } - data := templateData{ - JsonData: query.Result.JsonData, - SecureJsonData: query.Result.SecureJsonData.Decrypt(), - } - err = t.Execute(&contentBuf, data) - if err != nil { - ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err) - return - } - log.Debug("Adding header to proxy request. %s: %s", header.Name, contentBuf.String()) - req.Header.Add(header.Name, contentBuf.String()) - } - } - - return &httputil.ReverseProxy{Director: director} -} diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go new file mode 100644 index 00000000000..55aa0c013ce --- /dev/null +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -0,0 +1,99 @@ +package pluginproxy + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "text/template" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/util" +) + +type templateData struct { + JsonData map[string]interface{} + SecureJsonData map[string]string +} + +func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) { + result := http.Header{} + + query := m.GetAppSettingByAppIdQuery{OrgId: orgId, AppId: appId} + + if err := bus.Dispatch(&query); err != nil { + return nil, err + } + + data := templateData{ + JsonData: query.Result.JsonData, + SecureJsonData: query.Result.SecureJsonData.Decrypt(), + } + + for _, header := range route.Headers { + var contentBuf bytes.Buffer + t, err := template.New("content").Parse(header.Content) + if err != nil { + return nil, errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name)) + } + + err = t.Execute(&contentBuf, data) + if err != nil { + return nil, errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name)) + } + + log.Trace("Adding header to proxy request. %s: %s", header.Name, contentBuf.String()) + result.Add(header.Name, contentBuf.String()) + } + + return result, nil +} + +func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy { + targetUrl, _ := url.Parse(route.Url) + + director := func(req *http.Request) { + + req.URL.Scheme = targetUrl.Scheme + req.URL.Host = targetUrl.Host + req.Host = targetUrl.Host + + req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath) + + // clear cookie headers + req.Header.Del("Cookie") + req.Header.Del("Set-Cookie") + + //Create a HTTP header with the context in it. + ctxJson, err := json.Marshal(ctx.SignedInUser) + if err != nil { + ctx.JsonApiErr(500, "failed to marshal context to json.", err) + return + } + + req.Header.Add("X-Grafana-Context", string(ctxJson)) + + if len(route.Headers) > 0 { + headers, err := getHeaders(route, ctx.OrgId, appId) + if err != nil { + ctx.JsonApiErr(500, "Could not generate plugin route header", err) + return + } + + for key, value := range headers { + log.Info("setting key %v value %v", key, value[0]) + req.Header.Set(key, value[0]) + } + } + + } + + return &httputil.ReverseProxy{Director: director} +} diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go new file mode 100644 index 00000000000..0b6e9523ffd --- /dev/null +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -0,0 +1,42 @@ +package pluginproxy + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + . "github.com/smartystreets/goconvey/convey" +) + +func TestPluginProxy(t *testing.T) { + + Convey("When getting proxy headers", t, func() { + route := &plugins.AppPluginRoute{ + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, + } + + setting.SecretKey = "password" + + bus.AddHandler("test", func(query *m.GetAppSettingByAppIdQuery) error { + query.Result = &m.AppSettings{ + SecureJsonData: map[string][]byte{ + "key": util.Encrypt([]byte("123"), "password"), + }, + } + return nil + }) + + header, err := getHeaders(route, 1, "my-app") + So(err, ShouldBeNil) + + Convey("Should render header template", func() { + So(header.Get("x-header"), ShouldEqual, "my secret 123") + }) + }) + +} diff --git a/pkg/models/app_settings.go b/pkg/models/app_settings.go index 78d4c483f2b..6a7bbde694d 100644 --- a/pkg/models/app_settings.go +++ b/pkg/models/app_settings.go @@ -49,6 +49,14 @@ type UpdateAppSettingsCmd struct { OrgId int64 `json:"-"` } +func (cmd *UpdateAppSettingsCmd) GetEncryptedJsonData() SecureJsonData { + encrypted := make(SecureJsonData) + for key, data := range cmd.SecureJsonData { + encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey) + } + return encrypted +} + // --------------------- // QUERIES type GetAppSettingsQuery struct { diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index d59df9985a4..190ce8a9632 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -39,7 +39,6 @@ type AppPlugin struct { type AppPluginRoute struct { Path string `json:"path"` Method string `json:"method"` - ReqSignedIn bool `json:"reqSignedIn"` ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"` ReqRole models.RoleType `json:"reqRole"` Url string `json:"url"` diff --git a/pkg/services/sqlstore/app_settings.go b/pkg/services/sqlstore/app_settings.go index f454d2cc5ff..e7d8a90a495 100644 --- a/pkg/services/sqlstore/app_settings.go +++ b/pkg/services/sqlstore/app_settings.go @@ -42,18 +42,13 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error { sess.UseBool("enabled") sess.UseBool("pinned") if !exists { - // encrypt secureJsonData - secureJsonData := make(map[string][]byte) - for key, data := range cmd.SecureJsonData { - secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) - } app = m.AppSettings{ AppId: cmd.AppId, OrgId: cmd.OrgId, Enabled: cmd.Enabled, Pinned: cmd.Pinned, JsonData: cmd.JsonData, - SecureJsonData: secureJsonData, + SecureJsonData: cmd.GetEncryptedJsonData(), Created: time.Now(), Updated: time.Now(), } @@ -63,6 +58,7 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error { for key, data := range cmd.SecureJsonData { app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) } + app.SecureJsonData = cmd.GetEncryptedJsonData() app.Updated = time.Now() app.Enabled = cmd.Enabled app.JsonData = cmd.JsonData