Commit 175c651e by Torkel Ödegaard

fix(server side rendering): Fixed issues with server side rendering for alerting…

fix(server side rendering): Fixed issues with server side rendering for alerting & for auth proxy scenarios, fixes #6115, fixes #5906
parent bb77caa3
...@@ -25,6 +25,7 @@ ...@@ -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) * **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) * **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) * **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) # 3.1.2 (unreleased)
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
......
...@@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro ...@@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
url := ds.Url url := ds.Url
if ds.Access == m.DS_ACCESS_PROXY { 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{}{ var dsMap = map[string]interface{}{
......
package api package api
import ( import (
"fmt"
"strings" "strings"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
...@@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { ...@@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
locale = parts[0] 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{ var data = dtos.IndexViewData{
User: &dtos.CurrentUser{ User: &dtos.CurrentUser{
Id: c.UserId, Id: c.UserId,
...@@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { ...@@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Locale: locale, Locale: locale,
}, },
Settings: settings, Settings: settings,
AppUrl: setting.AppUrl, AppUrl: appUrl,
AppSubUrl: setting.AppSubUrl, AppSubUrl: appSubUrl,
GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId, GoogleTagManagerId: setting.GoogleTagManagerId,
BuildVersion: setting.BuildVersion, BuildVersion: setting.BuildVersion,
......
...@@ -6,35 +6,21 @@ import ( ...@@ -6,35 +6,21 @@ import (
"github.com/grafana/grafana/pkg/components/renderer" "github.com/grafana/grafana/pkg/components/renderer"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func RenderToPng(c *middleware.Context) { func RenderToPng(c *middleware.Context) {
queryReader := util.NewUrlQueryReader(c.Req.URL) queryReader := util.NewUrlQueryReader(c.Req.URL)
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) 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{ renderOpts := &renderer.RenderOpts{
Url: c.Params("*") + queryParams, Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"), Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"), Height: queryReader.Get("height", "400"),
SessionId: c.Session.ID(), OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"), Timeout: queryReader.Get("timeout", "30"),
} }
renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)
pngPath, err := renderer.RenderToPng(renderOpts) pngPath, err := renderer.RenderToPng(renderOpts)
if err != nil { if err != nil {
......
...@@ -12,16 +12,17 @@ import ( ...@@ -12,16 +12,17 @@ import (
"strconv" "strconv"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
type RenderOpts struct { type RenderOpts struct {
Url string Url string
Width string Width string
Height string Height string
SessionId string Timeout string
Timeout string OrgId int64
} }
var rendererLog log.Logger = log.New("png-renderer") var rendererLog log.Logger = log.New("png-renderer")
...@@ -34,14 +35,28 @@ func RenderToPng(params *RenderOpts) (string, error) { ...@@ -34,14 +35,28 @@ func RenderToPng(params *RenderOpts) (string, error) {
executable = executable + ".exe" 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)) binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png" pngPath = pngPath + ".png"
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, renderKey := middleware.AddRenderAuthKey(params.OrgId)
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, defer middleware.RemoveRenderAuthKey(renderKey)
"domain="+setting.Domain, "sessionid="+params.SessionId)
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() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
......
...@@ -22,6 +22,7 @@ type Context struct { ...@@ -22,6 +22,7 @@ type Context struct {
Session SessionStore Session SessionStore
IsSignedIn bool IsSignedIn bool
IsRenderCall bool
AllowAnonymous bool AllowAnonymous bool
Logger log.Logger Logger log.Logger
} }
...@@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler { ...@@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler {
// then init session and look for userId in session // then init session and look for userId in session
// then look for api key in session (special case for render calls via api) // then look for api key in session (special case for render calls via api)
// then test if anonymous access is enabled // then test if anonymous access is enabled
if initContextWithApiKey(ctx) || if initContextWithRenderAuth(ctx) ||
initContextWithApiKey(ctx) ||
initContextWithBasicAuth(ctx) || initContextWithBasicAuth(ctx) ||
initContextWithAuthProxy(ctx) || initContextWithAuthProxy(ctx) ||
initContextWithUserSessionCookie(ctx) || initContextWithUserSessionCookie(ctx) ||
initContextWithApiKeyFromSession(ctx) ||
initContextWithAnonymousUser(ctx) { initContextWithAnonymousUser(ctx) {
} }
...@@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool { ...@@ -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. // Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) { func (ctx *Context) Handle(status int, title string, err error) {
if err != nil { if err != nil {
......
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()
}
...@@ -13,7 +13,6 @@ import ( ...@@ -13,7 +13,6 @@ import (
const ( const (
SESS_KEY_USERID = "uid" SESS_KEY_USERID = "uid"
SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys
) )
var sessionManager *session.Manager var sessionManager *session.Manager
......
...@@ -69,11 +69,11 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error { ...@@ -69,11 +69,11 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
} }
renderOpts := &renderer.RenderOpts{ renderOpts := &renderer.RenderOpts{
Url: imageUrl, Url: imageUrl,
Width: "800", Width: "800",
Height: "400", Height: "400",
SessionId: "cef0256d482b4293", Timeout: "30",
Timeout: "30", OrgId: context.Rule.OrgId,
} }
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
......
...@@ -114,6 +114,10 @@ export class BackendSrv { ...@@ -114,6 +114,10 @@ export class BackendSrv {
var requestIsLocal = options.url.indexOf('/') === 0; var requestIsLocal = options.url.indexOf('/') === 0;
var firstAttempt = options.retry === 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) { if (requestIsLocal && options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization; options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization; delete options.headers.Authorization;
......
...@@ -12,17 +12,17 @@ ...@@ -12,17 +12,17 @@
params[parts[1]] = parts[2]; params[parts[1]] = parts[2];
}); });
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>"; var usage = "url=<url> png=<filename> width=<width> height=<height> renderKey=<key>";
if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) { if (!params.url || !params.png || !params.renderKey || !params.domain) {
console.log(usage); console.log(usage);
phantom.exit(); phantom.exit();
} }
phantom.addCookie({ phantom.addCookie({
'name': params.cookiename, 'name': 'renderKey',
'value': params.sessionid, 'value': params.renderKey,
'domain': params.domain 'domain': 'localhost',
}); });
page.viewportSize = { page.viewportSize = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment