diff --git a/conf/defaults.ini b/conf/defaults.ini index ba91f837601..f2d8ba6728d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -565,6 +565,17 @@ azure_auth_enabled = false # Use email lookup in addition to the unique ID provided by the IdP oauth_allow_insecure_email_lookup = false +# Set to true to include id of identity as a response header +id_response_header_enabled = false + +# Prefix used for the id response header, X-Grafana-Identity-Id +id_response_header_prefix = X-Grafana + +# List of identity namespaces to add id response headers for, separated by space. +# Available namespaces are user, api-key and service-account. +# The header value will encode the namespace ("user:", "api-key:", "service-account:") +id_response_header_namespaces = user api-key service-account + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/conf/sample.ini b/conf/sample.ini index b246b3a973c..f4dae0c14d2 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -554,6 +554,17 @@ # Use email lookup in addition to the unique ID provided by the IdP ;oauth_allow_insecure_email_lookup = false +# Set to true to include id of identity as a response header +;id_response_header_enabled = false + +# Prefix used for the id response header, X-Grafana-Identity-Id +;id_response_header_prefix = X-Grafana + +# List of identity namespaces to add id response headers for, separated by space. +# Available namespaces are user, api-key and service-account. +# The header value will encode the namespace ("user:", "api-key:", "service-account:") +;id_response_header_namespaces = user api-key service-account + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index ee90cef3648..6d9ef076911 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -4,7 +4,9 @@ package contexthandler import ( "context" "errors" + "fmt" "net/http" + "strconv" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -135,10 +137,51 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { attribute.Int64("userId", reqContext.UserID), )) + if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { + namespace, id := getNamespaceAndID(reqContext.SignedInUser) + reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(namespace, id)) + } + next.ServeHTTP(w, r) }) } +// TODO(kalleep): Refactor to user identity.Requester interface and methods after we have backported this +func getNamespaceAndID(user *user.SignedInUser) (string, string) { + var namespace, id string + if user.UserID > 0 && user.IsServiceAccount { + id = strconv.Itoa(int(user.UserID)) + namespace = "service-account" + } else if user.UserID > 0 { + id = strconv.Itoa(int(user.UserID)) + namespace = "user" + } else if user.ApiKeyID > 0 { + id = strconv.Itoa(int(user.ApiKeyID)) + namespace = "api-key" + } + + return namespace, id +} + +func (h *ContextHandler) addIDHeaderEndOfRequestFunc(namespace, id string) web.BeforeFunc { + return func(w web.ResponseWriter) { + if w.Written() { + return + } + + if namespace == "" || id == "" { + return + } + + if _, ok := h.Cfg.IDResponseHeaderNamespaces[namespace]; !ok { + return + } + + headerName := fmt.Sprintf("%s-Identity-Id", h.Cfg.IDResponseHeaderPrefix) + w.Header().Add(headerName, fmt.Sprintf("%s:%s", namespace, id)) + } +} + func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc { return func(w web.ResponseWriter) { if h.features.IsEnabled(reqContext.Req.Context(), featuremgmt.FlagClientTokenRotation) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 89222742b6c..c8ec8e61c70 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -285,6 +285,9 @@ type Cfg struct { AdminEmail string DisableLoginForm bool SignoutRedirectUrl string + IDResponseHeaderEnabled bool + IDResponseHeaderPrefix string + IDResponseHeaderNamespaces map[string]struct{} // Not documented & not supported // stand in until a more complete solution is implemented AuthConfigUIAdminAccess bool @@ -1607,6 +1610,17 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // Azure Auth AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false) cfg.AzureAuthEnabled = AzureAuthEnabled + + // ID response header + cfg.IDResponseHeaderEnabled = auth.Key("id_response_header_enabled").MustBool(false) + cfg.IDResponseHeaderPrefix = auth.Key("id_response_header_prefix").MustString("X-Grafana-") + + idHeaderNamespaces := util.SplitString(auth.Key("id_response_header_namespaces").MustString("")) + cfg.IDResponseHeaderNamespaces = make(map[string]struct{}, len(idHeaderNamespaces)) + for _, namespace := range idHeaderNamespaces { + cfg.IDResponseHeaderNamespaces[namespace] = struct{}{} + } + readAuthAzureADSettings(cfg) // Google Auth