mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			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")
 | |
| }
 |