mirror of
https://github.com/grafana/grafana.git
synced 2024-11-27 03:11:01 -06:00
d0a80c59f3
By storing render key in remote cache it will enable image renderer to use public facing url or load balancer url to render images and thereby remove the requirement of image renderer having to use the url of the originating Grafana instance when running HA setup (multiple Grafana instances). Fixes #17704 Ref grafana/grafana-image-renderer#91
332 lines
9.0 KiB
Go
332 lines
9.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
macaron "gopkg.in/macaron.v1"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/components/apikeygen"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/rendering"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var getTime = time.Now
|
|
|
|
const (
|
|
errStringInvalidUsernamePassword = "Invalid username or password"
|
|
errStringInvalidAPIKey = "Invalid API key"
|
|
)
|
|
|
|
var (
|
|
ReqGrafanaAdmin = Auth(&AuthOptions{
|
|
ReqSignedIn: true,
|
|
ReqGrafanaAdmin: true,
|
|
})
|
|
ReqSignedIn = Auth(&AuthOptions{ReqSignedIn: true})
|
|
ReqEditorRole = RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN)
|
|
ReqOrgAdmin = RoleAuth(models.ROLE_ADMIN)
|
|
)
|
|
|
|
func GetContextHandler(
|
|
ats models.UserTokenService,
|
|
remoteCache *remotecache.RemoteCache,
|
|
renderService rendering.Service,
|
|
) macaron.Handler {
|
|
return func(c *macaron.Context) {
|
|
ctx := &models.ReqContext{
|
|
Context: c,
|
|
SignedInUser: &models.SignedInUser{},
|
|
IsSignedIn: false,
|
|
AllowAnonymous: false,
|
|
SkipCache: false,
|
|
Logger: log.New("context"),
|
|
}
|
|
|
|
orgId := int64(0)
|
|
orgIdHeader := ctx.Req.Header.Get("X-Grafana-Org-Id")
|
|
if orgIdHeader != "" {
|
|
orgId, _ = strconv.ParseInt(orgIdHeader, 10, 64)
|
|
}
|
|
|
|
// the order in which these are tested are important
|
|
// look for api key in Authorization header first
|
|
// 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
|
|
switch {
|
|
case initContextWithRenderAuth(ctx, renderService):
|
|
case initContextWithApiKey(ctx):
|
|
case initContextWithBasicAuth(ctx, orgId):
|
|
case initContextWithAuthProxy(remoteCache, ctx, orgId):
|
|
case initContextWithToken(ats, ctx, orgId):
|
|
case initContextWithAnonymousUser(ctx):
|
|
}
|
|
|
|
ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login)
|
|
ctx.Data["ctx"] = ctx
|
|
|
|
c.Map(ctx)
|
|
|
|
// update last seen every 5min
|
|
if ctx.ShouldUpdateLastSeenAt() {
|
|
ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId)
|
|
if err := bus.Dispatch(&models.UpdateUserLastSeenAtCommand{UserId: ctx.UserId}); err != nil {
|
|
ctx.Logger.Error("Failed to update last_seen_at", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func initContextWithAnonymousUser(ctx *models.ReqContext) bool {
|
|
if !setting.AnonymousEnabled {
|
|
return false
|
|
}
|
|
|
|
orgQuery := models.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
|
|
if err := bus.Dispatch(&orgQuery); err != nil {
|
|
log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
|
|
return false
|
|
}
|
|
|
|
ctx.IsSignedIn = false
|
|
ctx.AllowAnonymous = true
|
|
ctx.SignedInUser = &models.SignedInUser{IsAnonymous: true}
|
|
ctx.OrgRole = models.RoleType(setting.AnonymousOrgRole)
|
|
ctx.OrgId = orgQuery.Result.Id
|
|
ctx.OrgName = orgQuery.Result.Name
|
|
return true
|
|
}
|
|
|
|
func initContextWithApiKey(ctx *models.ReqContext) bool {
|
|
var keyString string
|
|
if keyString = getApiKey(ctx); keyString == "" {
|
|
return false
|
|
}
|
|
|
|
// base64 decode key
|
|
decoded, err := apikeygen.Decode(keyString)
|
|
if err != nil {
|
|
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
|
|
return true
|
|
}
|
|
|
|
// fetch key
|
|
keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
|
|
if err := bus.Dispatch(&keyQuery); err != nil {
|
|
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
|
|
return true
|
|
}
|
|
|
|
apikey := keyQuery.Result
|
|
|
|
// validate api key
|
|
isValid, err := apikeygen.IsValid(decoded, apikey.Key)
|
|
if err != nil {
|
|
ctx.JsonApiErr(500, "Validating API key failed", err)
|
|
return true
|
|
}
|
|
if !isValid {
|
|
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
|
|
return true
|
|
}
|
|
|
|
// check for expiration
|
|
if apikey.Expires != nil && *apikey.Expires <= getTime().Unix() {
|
|
ctx.JsonApiErr(401, "Expired API key", err)
|
|
return true
|
|
}
|
|
|
|
ctx.IsSignedIn = true
|
|
ctx.SignedInUser = &models.SignedInUser{}
|
|
ctx.OrgRole = apikey.Role
|
|
ctx.ApiKeyId = apikey.Id
|
|
ctx.OrgId = apikey.OrgId
|
|
return true
|
|
}
|
|
|
|
func initContextWithBasicAuth(ctx *models.ReqContext, orgId int64) bool {
|
|
if !setting.BasicAuthEnabled {
|
|
return false
|
|
}
|
|
|
|
header := ctx.Req.Header.Get("Authorization")
|
|
if header == "" {
|
|
return false
|
|
}
|
|
|
|
username, password, err := util.DecodeBasicAuthHeader(header)
|
|
if err != nil {
|
|
ctx.JsonApiErr(401, "Invalid Basic Auth Header", err)
|
|
return true
|
|
}
|
|
|
|
authQuery := models.LoginUserQuery{
|
|
Username: username,
|
|
Password: password,
|
|
}
|
|
if err := bus.Dispatch(&authQuery); err != nil {
|
|
ctx.Logger.Debug(
|
|
"Failed to authorize the user",
|
|
"username", username,
|
|
)
|
|
|
|
ctx.JsonApiErr(401, errStringInvalidUsernamePassword, err)
|
|
return true
|
|
}
|
|
|
|
user := authQuery.User
|
|
|
|
query := models.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId}
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
ctx.Logger.Error(
|
|
"Failed at user signed in",
|
|
"id", user.Id,
|
|
"org", orgId,
|
|
)
|
|
ctx.JsonApiErr(401, errStringInvalidUsernamePassword, err)
|
|
return true
|
|
}
|
|
|
|
ctx.SignedInUser = query.Result
|
|
ctx.IsSignedIn = true
|
|
return true
|
|
}
|
|
|
|
func initContextWithToken(authTokenService models.UserTokenService, ctx *models.ReqContext, orgID int64) bool {
|
|
if setting.LoginCookieName == "" {
|
|
return false
|
|
}
|
|
|
|
rawToken := ctx.GetCookie(setting.LoginCookieName)
|
|
if rawToken == "" {
|
|
return false
|
|
}
|
|
|
|
token, err := authTokenService.LookupToken(ctx.Req.Context(), rawToken)
|
|
if err != nil {
|
|
ctx.Logger.Error("Failed to look up user based on cookie", "error", err)
|
|
WriteSessionCookie(ctx, "", -1)
|
|
return false
|
|
}
|
|
|
|
query := models.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
ctx.Logger.Error("Failed to get user with id", "userId", token.UserId, "error", err)
|
|
return false
|
|
}
|
|
|
|
ctx.SignedInUser = query.Result
|
|
ctx.IsSignedIn = true
|
|
ctx.UserToken = token
|
|
|
|
// Rotate the token just before we write response headers to ensure there is no delay between
|
|
// the new token being generated and the client receiving it.
|
|
ctx.Resp.Before(rotateEndOfRequestFunc(ctx, authTokenService, token))
|
|
|
|
return true
|
|
}
|
|
|
|
func rotateEndOfRequestFunc(ctx *models.ReqContext, authTokenService models.UserTokenService, token *models.UserToken) macaron.BeforeFunc {
|
|
return func(w macaron.ResponseWriter) {
|
|
// if response has already been written, skip.
|
|
if w.Written() {
|
|
return
|
|
}
|
|
|
|
// if the request is cancelled by the client we should not try
|
|
// to rotate the token since the client would not accept any result.
|
|
if ctx.Context.Req.Context().Err() == context.Canceled {
|
|
return
|
|
}
|
|
|
|
rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
|
if err != nil {
|
|
ctx.Logger.Error("Failed to rotate token", "error", err)
|
|
return
|
|
}
|
|
|
|
if rotated {
|
|
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
|
|
}
|
|
}
|
|
}
|
|
|
|
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) {
|
|
if setting.Env == setting.DEV {
|
|
ctx.Logger.Info("New token", "unhashed token", value)
|
|
}
|
|
|
|
var maxAge int
|
|
if maxLifetimeDays <= 0 {
|
|
maxAge = -1
|
|
} else {
|
|
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour
|
|
maxAge = int(maxAgeHours.Seconds())
|
|
}
|
|
|
|
WriteCookie(ctx.Resp, setting.LoginCookieName, url.QueryEscape(value), maxAge, newCookieOptions)
|
|
}
|
|
|
|
func AddDefaultResponseHeaders() macaron.Handler {
|
|
return func(ctx *macaron.Context) {
|
|
ctx.Resp.Before(func(w macaron.ResponseWriter) {
|
|
// if response has already been written, skip.
|
|
if w.Written() {
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(ctx.Req.URL.Path, "/api/datasources/proxy/") {
|
|
AddNoCacheHeaders(ctx.Resp)
|
|
}
|
|
|
|
if !setting.AllowEmbedding {
|
|
AddXFrameOptionsDenyHeader(w)
|
|
}
|
|
|
|
AddSecurityHeaders(w)
|
|
})
|
|
}
|
|
}
|
|
|
|
// AddSecurityHeaders adds various HTTP(S) response headers that enable various security protections behaviors in the client's browser.
|
|
func AddSecurityHeaders(w macaron.ResponseWriter) {
|
|
if (setting.Protocol == setting.HTTPS || setting.Protocol == setting.HTTP2) && setting.StrictTransportSecurity {
|
|
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge)}
|
|
if setting.StrictTransportSecurityPreload {
|
|
strictHeaderValues = append(strictHeaderValues, "preload")
|
|
}
|
|
if setting.StrictTransportSecuritySubDomains {
|
|
strictHeaderValues = append(strictHeaderValues, "includeSubDomains")
|
|
}
|
|
w.Header().Add("Strict-Transport-Security", strings.Join(strictHeaderValues, "; "))
|
|
}
|
|
|
|
if setting.ContentTypeProtectionHeader {
|
|
w.Header().Add("X-Content-Type-Options", "nosniff")
|
|
}
|
|
|
|
if setting.XSSProtectionHeader {
|
|
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
|
}
|
|
}
|
|
|
|
func AddNoCacheHeaders(w macaron.ResponseWriter) {
|
|
w.Header().Add("Cache-Control", "no-cache")
|
|
w.Header().Add("Pragma", "no-cache")
|
|
w.Header().Add("Expires", "-1")
|
|
}
|
|
|
|
func AddXFrameOptionsDenyHeader(w macaron.ResponseWriter) {
|
|
w.Header().Add("X-Frame-Options", "deny")
|
|
}
|