From 175c651e65a853874a1a272ecf22afeedb3d13d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Sep 2016 12:29:53 +0200 Subject: [PATCH] fix(server side rendering): Fixed issues with server side rendering for alerting & for auth proxy scenarios, fixes #6115, fixes #5906 --- CHANGELOG.md | 1 + pkg/api/frontendsettings.go | 2 +- pkg/api/index.go | 15 ++++++- pkg/api/render.go | 24 +++-------- pkg/components/renderer/renderer.go | 31 ++++++++++---- pkg/middleware/middleware.go | 28 ++----------- pkg/middleware/render_auth.go | 55 +++++++++++++++++++++++++ pkg/middleware/session.go | 1 - pkg/services/alerting/notifier.go | 10 ++--- public/app/core/services/backend_srv.ts | 4 ++ vendor/phantomjs/render.js | 10 ++--- 11 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 pkg/middleware/render_auth.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c842b3b031a..3fc33b0c0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) * **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769) * **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887) +* **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906) # 3.1.2 (unreleased) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3a019e80c49..5a324aa1331 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro url := ds.Url if ds.Access == m.DS_ACCESS_PROXY { - url = setting.AppSubUrl + "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10) + url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10) } var dsMap = map[string]interface{}{ diff --git a/pkg/api/index.go b/pkg/api/index.go index e9d784cb652..063e91ef5da 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "strings" "github.com/grafana/grafana/pkg/api/dtos" @@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { locale = parts[0] } + appUrl := setting.AppUrl + appSubUrl := setting.AppSubUrl + + // special case when doing localhost call from phantomjs + if c.IsRenderCall { + appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) + appSubUrl = "" + settings["appSubUrl"] = "" + } + var data = dtos.IndexViewData{ User: &dtos.CurrentUser{ Id: c.UserId, @@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Locale: locale, }, Settings: settings, - AppUrl: setting.AppUrl, - AppSubUrl: setting.AppSubUrl, + AppUrl: appUrl, + AppSubUrl: appSubUrl, GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, diff --git a/pkg/api/render.go b/pkg/api/render.go index 65c1499d0c5..ab794e7ce3e 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -6,35 +6,21 @@ import ( "github.com/grafana/grafana/pkg/components/renderer" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) func RenderToPng(c *middleware.Context) { queryReader := util.NewUrlQueryReader(c.Req.URL) queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) - sessionId := c.Session.ID() - - // Handle api calls authenticated without session - if sessionId == "" && c.ApiKeyId != 0 { - c.Session.Start(c) - c.Session.Set(middleware.SESS_KEY_APIKEY, c.ApiKeyId) - // release will make sure the new session is persisted before - // we spin up phantomjs - c.Session.Release() - // cleanup session after render is complete - defer func() { c.Session.Destory(c) }() - } renderOpts := &renderer.RenderOpts{ - Url: c.Params("*") + queryParams, - Width: queryReader.Get("width", "800"), - Height: queryReader.Get("height", "400"), - SessionId: c.Session.ID(), - Timeout: queryReader.Get("timeout", "30"), + Url: c.Params("*") + queryParams, + Width: queryReader.Get("width", "800"), + Height: queryReader.Get("height", "400"), + OrgId: c.OrgId, + Timeout: queryReader.Get("timeout", "30"), } - renderOpts.Url = setting.ToAbsUrl(renderOpts.Url) pngPath, err := renderer.RenderToPng(renderOpts) if err != nil { diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index ad8f76e03aa..87791bbb1a5 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -12,16 +12,17 @@ import ( "strconv" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) type RenderOpts struct { - Url string - Width string - Height string - SessionId string - Timeout string + Url string + Width string + Height string + Timeout string + OrgId int64 } var rendererLog log.Logger = log.New("png-renderer") @@ -34,14 +35,28 @@ func RenderToPng(params *RenderOpts) (string, error) { executable = executable + ".exe" } + params.Url = fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Url) + binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable)) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath = pngPath + ".png" - cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, - "height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, - "domain="+setting.Domain, "sessionid="+params.SessionId) + renderKey := middleware.AddRenderAuthKey(params.OrgId) + defer middleware.RemoveRenderAuthKey(renderKey) + + cmdArgs := []string{ + "--ignore-ssl-errors=true", + scriptPath, + "url=" + params.Url, + "width=" + params.Width, + "height=" + params.Height, + "png=" + pngPath, + "domain=" + setting.Domain, + "renderKey=" + renderKey, + } + + cmd := exec.Command(binPath, cmdArgs...) stdout, err := cmd.StdoutPipe() if err != nil { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index df1768e1c3a..cb3f4480821 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -22,6 +22,7 @@ type Context struct { Session SessionStore IsSignedIn bool + IsRenderCall bool AllowAnonymous bool Logger log.Logger } @@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler { // then init session and look for userId in session // then look for api key in session (special case for render calls via api) // then test if anonymous access is enabled - if initContextWithApiKey(ctx) || + if initContextWithRenderAuth(ctx) || + initContextWithApiKey(ctx) || initContextWithBasicAuth(ctx) || initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(ctx) || - initContextWithApiKeyFromSession(ctx) || initContextWithAnonymousUser(ctx) { } @@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool { } } -// special case for panel render calls with api key -func initContextWithApiKeyFromSession(ctx *Context) bool { - keyId := ctx.Session.Get(SESS_KEY_APIKEY) - if keyId == nil { - return false - } - - keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)} - if err := bus.Dispatch(&keyQuery); err != nil { - ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err) - return false - } else { - apikey := keyQuery.Result - - ctx.IsSignedIn = true - ctx.SignedInUser = &m.SignedInUser{} - ctx.OrgRole = apikey.Role - ctx.ApiKeyId = apikey.Id - ctx.OrgId = apikey.OrgId - return true - } -} - // Handle handles and logs error by given status. func (ctx *Context) Handle(status int, title string, err error) { if err != nil { diff --git a/pkg/middleware/render_auth.go b/pkg/middleware/render_auth.go new file mode 100644 index 00000000000..3a57660c9bf --- /dev/null +++ b/pkg/middleware/render_auth.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "sync" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +var renderKeysLock sync.Mutex +var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser) + +func initContextWithRenderAuth(ctx *Context) bool { + key := ctx.GetCookie("renderKey") + if key == "" { + return false + } + + renderKeysLock.Lock() + defer renderKeysLock.Unlock() + + if renderUser, exists := renderKeys[key]; !exists { + ctx.JsonApiErr(401, "Invalid Render Key", nil) + return true + } else { + + ctx.IsSignedIn = true + ctx.SignedInUser = renderUser + ctx.IsRenderCall = true + return true + } +} + +type renderContextFunc func(key string) (string, error) + +func AddRenderAuthKey(orgId int64) string { + renderKeysLock.Lock() + + key := util.GetRandomString(32) + + renderKeys[key] = &m.SignedInUser{ + OrgId: orgId, + OrgRole: m.ROLE_VIEWER, + } + + renderKeysLock.Unlock() + + return key +} + +func RemoveRenderAuthKey(key string) { + renderKeysLock.Lock() + delete(renderKeys, key) + renderKeysLock.Unlock() +} diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 583c57b85a5..ee6462be37a 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -13,7 +13,6 @@ import ( const ( SESS_KEY_USERID = "uid" - SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys ) var sessionManager *session.Manager diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 61cc0bc55d3..06828356eaf 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -69,11 +69,11 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error { } renderOpts := &renderer.RenderOpts{ - Url: imageUrl, - Width: "800", - Height: "400", - SessionId: "cef0256d482b4293", - Timeout: "30", + Url: imageUrl, + Width: "800", + Height: "400", + Timeout: "30", + OrgId: context.Rule.OrgId, } if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index fdc2b6cb974..1e620e88216 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -114,6 +114,10 @@ export class BackendSrv { var requestIsLocal = options.url.indexOf('/') === 0; var firstAttempt = options.retry === 0; + if (requestIsLocal && !options.hasSubUrl && options.retry === 0) { + options.url = config.appSubUrl + options.url; + } + if (requestIsLocal && options.headers && options.headers.Authorization) { options.headers['X-DS-Authorization'] = options.headers.Authorization; delete options.headers.Authorization; diff --git a/vendor/phantomjs/render.js b/vendor/phantomjs/render.js index 3e10ee852f9..2f62bfce955 100644 --- a/vendor/phantomjs/render.js +++ b/vendor/phantomjs/render.js @@ -12,17 +12,17 @@ params[parts[1]] = parts[2]; }); - var usage = "url= png= width= height= cookiename= sessionid= domain="; + var usage = "url= png= width= height= renderKey="; - if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) { + if (!params.url || !params.png || !params.renderKey || !params.domain) { console.log(usage); phantom.exit(); } phantom.addCookie({ - 'name': params.cookiename, - 'value': params.sessionid, - 'domain': params.domain + 'name': 'renderKey', + 'value': params.renderKey, + 'domain': 'localhost', }); page.viewportSize = {