2025-04-01 10:38:02 +09:00

231 lines
8.1 KiB
Go

// Package contexthandler contains the ContextHandler service.
package contexthandler
import (
"context"
"fmt"
"net/http"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
claims "github.com/grafana/authlib/types"
authnClients "github.com/grafana/grafana/pkg/services/authn/clients"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"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/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, authenticator authn.Authenticator, features featuremgmt.FeatureToggles,
) *ContextHandler {
return &ContextHandler{
cfg: cfg,
authenticator: authenticator,
features: features,
}
}
// ContextHandler is a middleware.
type ContextHandler struct {
cfg *setting.Cfg
authenticator authn.Authenticator
features featuremgmt.FeatureToggles
}
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()
// Don't modify context so that the auth middleware span doesn't get propagated as a parent elsewhere
_, span := tracing.Start(ctx, "Auth - Middleware")
reqContext := &contextmodel.ReqContext{
Context: web.FromContext(ctx),
SignedInUser: &user.SignedInUser{
Permissions: map[int64]map[string][]string{},
},
IsSignedIn: false,
AllowAnonymous: false,
SkipDSCache: false,
Logger: log.New("context"),
UseSessionStorageRedirect: h.features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection),
}
// inject ReqContext in the context
ctx = context.WithValue(ctx, reqContextKey{}, reqContext)
// store list of possible auth header in context
ctx = WithAuthHTTPHeaders(ctx, h.cfg)
// Set the context for the http.Request.Context
// This modifies both r and reqContext.Req since they point to the same value
*reqContext.Req = *reqContext.Req.WithContext(ctx)
ctx = trace.ContextWithSpan(reqContext.Req.Context(), span)
traceID := tracing.TraceIDFromContext(ctx, false)
if traceID != "" {
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
}
id, err := h.authenticator.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req})
if err != nil {
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
reqContext.LookupTokenErr = err
} else {
reqContext.SignedInUser = id.SignedInUser()
reqContext.UserToken = id.SessionToken
reqContext.IsSignedIn = !reqContext.SignedInUser.IsAnonymous
reqContext.AllowAnonymous = reqContext.SignedInUser.IsAnonymous
reqContext.IsRenderCall = id.IsAuthenticatedBy(login.RenderModule)
ctx = identity.WithRequester(ctx, id)
}
h.excludeSensitiveHeadersFromRequest(reqContext.Req)
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
span.AddEvent("user", trace.WithAttributes(
attribute.String("uname", reqContext.Login),
attribute.Int64("orgId", reqContext.OrgID),
attribute.Int64("userId", reqContext.UserID),
))
if h.cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil {
reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser))
}
// End the span to make next handlers not wrapped within middleware span
span.End()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (h *ContextHandler) excludeSensitiveHeadersFromRequest(req *http.Request) {
req.Header.Del(authnClients.ExtJWTAuthenticationHeaderName)
req.Header.Del(authnClients.ExtJWTAuthorizationHeaderName)
}
func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) web.BeforeFunc {
return func(w web.ResponseWriter) {
if w.Written() {
return
}
id, _ := ident.GetInternalID()
if !ident.IsIdentityType(
claims.TypeUser,
claims.TypeServiceAccount,
claims.TypeAPIKey,
) || id == 0 {
return
}
if _, ok := h.cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok {
return
}
headerName := fmt.Sprintf("%s-Identity-Id", h.cfg.IDResponseHeaderPrefix)
w.Header().Add(headerName, ident.GetID())
}
}
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")
// remove X-Grafana-Device-Id as it is only used for auth in authn clients.
list.Items = append(list.Items, "X-Grafana-Device-Id")
// if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization
if cfg.JWTAuth.Enabled && cfg.JWTAuth.HeaderName != "" && cfg.JWTAuth.HeaderName != "Authorization" {
list.Items = append(list.Items, cfg.JWTAuth.HeaderName)
}
// if auth proxy is enabled add the main proxy header and all configured headers
if cfg.AuthProxy.Enabled {
list.Items = append(list.Items, cfg.AuthProxy.HeaderName)
for _, header := range cfg.AuthProxy.Headers {
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
}