mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> Co-authored-by: Ezequiel Victorero <ezequiel.victorero@grafana.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
205 lines
7.1 KiB
Go
205 lines
7.1 KiB
Go
// Package contexthandler contains the ContextHandler service.
|
|
package contexthandler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
|
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/login"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureManager, authnService authn.Service,
|
|
) *ContextHandler {
|
|
return &ContextHandler{
|
|
Cfg: cfg,
|
|
tracer: tracer,
|
|
features: features,
|
|
authnService: authnService,
|
|
}
|
|
}
|
|
|
|
// ContextHandler is a middleware.
|
|
type ContextHandler struct {
|
|
Cfg *setting.Cfg
|
|
tracer tracing.Tracer
|
|
features *featuremgmt.FeatureManager
|
|
authnService authn.Service
|
|
}
|
|
|
|
type reqContextKey = ctxkey.Key
|
|
|
|
// FromContext returns the ReqContext value stored in a context.Context, if any.
|
|
func FromContext(c context.Context) *contextmodel.ReqContext {
|
|
if reqCtx, ok := c.Value(reqContextKey{}).(*contextmodel.ReqContext); ok {
|
|
return reqCtx
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CopyWithReqContext returns a copy of the parent context with a semi-shallow copy of the ReqContext as a value.
|
|
// The ReqContexts's *web.Context is deep copied so that headers are thread-safe; additional properties are shallow copied and should be treated as read-only.
|
|
func CopyWithReqContext(ctx context.Context) context.Context {
|
|
origReqCtx := FromContext(ctx)
|
|
if origReqCtx == nil {
|
|
return ctx
|
|
}
|
|
|
|
webCtx := &web.Context{
|
|
Req: origReqCtx.Req.Clone(ctx),
|
|
Resp: web.NewResponseWriter(origReqCtx.Req.Method, response.CreateNormalResponse(http.Header{}, []byte{}, 0)),
|
|
}
|
|
reqCtx := &contextmodel.ReqContext{
|
|
Context: webCtx,
|
|
SignedInUser: origReqCtx.SignedInUser,
|
|
UserToken: origReqCtx.UserToken,
|
|
IsSignedIn: origReqCtx.IsSignedIn,
|
|
IsRenderCall: origReqCtx.IsRenderCall,
|
|
AllowAnonymous: origReqCtx.AllowAnonymous,
|
|
SkipDSCache: origReqCtx.SkipDSCache,
|
|
SkipQueryCache: origReqCtx.SkipQueryCache,
|
|
Logger: origReqCtx.Logger,
|
|
Error: origReqCtx.Error,
|
|
RequestNonce: origReqCtx.RequestNonce,
|
|
PublicDashboardAccessToken: origReqCtx.PublicDashboardAccessToken,
|
|
LookupTokenErr: origReqCtx.LookupTokenErr,
|
|
}
|
|
return context.WithValue(ctx, reqContextKey{}, reqCtx)
|
|
}
|
|
|
|
// Middleware provides a middleware to initialize the request context.
|
|
func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
mContext := web.FromContext(ctx)
|
|
_, span := h.tracer.Start(ctx, "Auth - Middleware")
|
|
defer span.End()
|
|
|
|
reqContext := &contextmodel.ReqContext{
|
|
Context: mContext,
|
|
SignedInUser: &user.SignedInUser{
|
|
Permissions: map[int64]map[string][]string{},
|
|
},
|
|
IsSignedIn: false,
|
|
AllowAnonymous: false,
|
|
SkipDSCache: false,
|
|
Logger: log.New("context"),
|
|
}
|
|
|
|
// Inject ReqContext into http.Request.Context
|
|
*r = *r.WithContext(context.WithValue(ctx, reqContextKey{}, reqContext))
|
|
// store list of possible auth header in context
|
|
*reqContext.Req = *reqContext.Req.WithContext(WithAuthHTTPHeaders(reqContext.Req.Context(), h.Cfg))
|
|
|
|
traceID := tracing.TraceIDFromContext(mContext.Req.Context(), false)
|
|
if traceID != "" {
|
|
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
|
}
|
|
|
|
identity, err := h.authnService.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
|
|
if err != nil {
|
|
if errors.Is(err, auth.ErrInvalidSessionToken) || errors.Is(err, authn.ErrExpiredAccessToken) {
|
|
// Burn the cookie in case of invalid, expired or missing token
|
|
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
|
|
}
|
|
|
|
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
|
|
reqContext.LookupTokenErr = err
|
|
} else {
|
|
reqContext.SignedInUser = identity.SignedInUser()
|
|
reqContext.UserToken = identity.SessionToken
|
|
reqContext.IsSignedIn = !identity.IsAnonymous
|
|
reqContext.AllowAnonymous = identity.IsAnonymous
|
|
reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule
|
|
}
|
|
|
|
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
|
|
span.AddEvents(
|
|
[]string{"uname", "orgId", "userId"},
|
|
[]tracing.EventValue{
|
|
{Str: reqContext.Login},
|
|
{Num: reqContext.OrgID},
|
|
{Num: reqContext.UserID}},
|
|
)
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc {
|
|
return func(w web.ResponseWriter) {
|
|
if h.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
|
|
return
|
|
}
|
|
|
|
if w.Written() {
|
|
reqContext.Logger.Debug("Response written, skipping invalid cookie delete")
|
|
return
|
|
}
|
|
|
|
reqContext.Logger.Debug("Expiring invalid cookie")
|
|
authn.DeleteSessionCookie(reqContext.Resp, h.Cfg)
|
|
}
|
|
}
|
|
|
|
type authHTTPHeaderListContextKey struct{}
|
|
|
|
var authHTTPHeaderListKey = authHTTPHeaderListContextKey{}
|
|
|
|
// AuthHTTPHeaderList used to record HTTP headers that being when verifying authentication
|
|
// of an incoming HTTP request.
|
|
type AuthHTTPHeaderList struct {
|
|
Items []string
|
|
}
|
|
|
|
// WithAuthHTTPHeaders returns a new context in which all possible configured auth header will be included
|
|
// and later retrievable by AuthHTTPHeaderListFromContext.
|
|
func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context {
|
|
list := AuthHTTPHeaderListFromContext(ctx)
|
|
if list == nil {
|
|
list = &AuthHTTPHeaderList{
|
|
Items: []string{},
|
|
}
|
|
}
|
|
|
|
// used by basic auth, api keys and potentially jwt auth
|
|
list.Items = append(list.Items, "Authorization")
|
|
|
|
// if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization
|
|
if cfg.JWTAuthEnabled && cfg.JWTAuthHeaderName != "" && cfg.JWTAuthHeaderName != "Authorization" {
|
|
list.Items = append(list.Items, cfg.JWTAuthHeaderName)
|
|
}
|
|
|
|
// if auth proxy is enabled add the main proxy header and all configured headers
|
|
if cfg.AuthProxyEnabled {
|
|
list.Items = append(list.Items, cfg.AuthProxyHeaderName)
|
|
for _, header := range cfg.AuthProxyHeaders {
|
|
if header != "" {
|
|
list.Items = append(list.Items, header)
|
|
}
|
|
}
|
|
}
|
|
|
|
return context.WithValue(ctx, authHTTPHeaderListKey, list)
|
|
}
|
|
|
|
// AuthHTTPHeaderListFromContext returns the AuthHTTPHeaderList in a context.Context, if any,
|
|
// and will include any HTTP headers used when verifying authentication of an incoming HTTP request.
|
|
func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList {
|
|
if list, ok := c.Value(authHTTPHeaderListKey).(*AuthHTTPHeaderList); ok {
|
|
return list
|
|
}
|
|
return nil
|
|
}
|