Move middleware context handler logic to service (#29605)
* middleware: Move context handler to own service Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Emil Tullsted <sakjur@users.noreply.github.com> Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
co-authored by
Emil Tullsted
Will Browne
parent
d0f52d5334
commit
12661e8a9d
+15
-272
@@ -1,32 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/network"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"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 (
|
||||
@@ -39,244 +20,7 @@ var (
|
||||
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 != "" {
|
||||
orgIDParsed, err := strconv.ParseInt(orgIDHeader, 10, 64)
|
||||
if err == nil {
|
||||
orgID = orgIDParsed
|
||||
}
|
||||
}
|
||||
|
||||
// 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.Errorf(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,
|
||||
"err", err,
|
||||
)
|
||||
|
||||
if errors.Is(err, models.ErrUserNotFound) {
|
||||
err = login.ErrInvalidCredentials
|
||||
}
|
||||
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 errors.Is(ctx.Context.Req.Context().Err(), context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
addr := ctx.RemoteAddr()
|
||||
ip, err := network.GetIPFromAddress(addr)
|
||||
if err != nil {
|
||||
ctx.Logger.Debug("Failed to get client IP address", "addr", addr, "err", err)
|
||||
ip = nil
|
||||
}
|
||||
rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ip, ctx.Req.UserAgent())
|
||||
if err != nil {
|
||||
ctx.Logger.Error("Failed to rotate token", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if rotated {
|
||||
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddDefaultResponseHeaders() macaron.Handler {
|
||||
func AddDefaultResponseHeaders(cfg *setting.Cfg) macaron.Handler {
|
||||
return func(ctx *macaron.Context) {
|
||||
ctx.Resp.Before(func(w macaron.ResponseWriter) {
|
||||
// if response has already been written, skip.
|
||||
@@ -285,47 +29,46 @@ func AddDefaultResponseHeaders() macaron.Handler {
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(ctx.Req.URL.Path, "/api/datasources/proxy/") {
|
||||
AddNoCacheHeaders(ctx.Resp)
|
||||
addNoCacheHeaders(ctx.Resp)
|
||||
}
|
||||
|
||||
if !setting.AllowEmbedding {
|
||||
AddXFrameOptionsDenyHeader(w)
|
||||
if !cfg.AllowEmbedding {
|
||||
addXFrameOptionsDenyHeader(w)
|
||||
}
|
||||
|
||||
AddSecurityHeaders(w)
|
||||
addSecurityHeaders(w, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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.HTTPSScheme || setting.Protocol == setting.HTTP2Scheme) &&
|
||||
setting.StrictTransportSecurity {
|
||||
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge)}
|
||||
if setting.StrictTransportSecurityPreload {
|
||||
// addSecurityHeaders adds HTTP(S) response headers that enable various security protections in the client's browser.
|
||||
func addSecurityHeaders(w macaron.ResponseWriter, cfg *setting.Cfg) {
|
||||
if (cfg.Protocol == setting.HTTPSScheme || cfg.Protocol == setting.HTTP2Scheme) && cfg.StrictTransportSecurity {
|
||||
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", cfg.StrictTransportSecurityMaxAge)}
|
||||
if cfg.StrictTransportSecurityPreload {
|
||||
strictHeaderValues = append(strictHeaderValues, "preload")
|
||||
}
|
||||
if setting.StrictTransportSecuritySubDomains {
|
||||
if cfg.StrictTransportSecuritySubDomains {
|
||||
strictHeaderValues = append(strictHeaderValues, "includeSubDomains")
|
||||
}
|
||||
w.Header().Add("Strict-Transport-Security", strings.Join(strictHeaderValues, "; "))
|
||||
}
|
||||
|
||||
if setting.ContentTypeProtectionHeader {
|
||||
if cfg.ContentTypeProtectionHeader {
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
|
||||
if setting.XSSProtectionHeader {
|
||||
if cfg.XSSProtectionHeader {
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
}
|
||||
}
|
||||
|
||||
func AddNoCacheHeaders(w macaron.ResponseWriter) {
|
||||
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) {
|
||||
func addXFrameOptionsDenyHeader(w macaron.ResponseWriter) {
|
||||
w.Header().Add("X-Frame-Options", "deny")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user