/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
pkg/api/index.go
334 строки
12 KB
Leon Sorokin
FeatureToggle: Move useLuxon flag to backend, it must run pre app-init (#130457)
11 авг 2026, 18:22
Не верифицирован
11 авг 2026, 18:22
d8f244c
Код
Авторство
О чём код?
package api import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "html/template" "net/http" "strings" "time" "github.com/open-feature/go-sdk/openfeature" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/webassets" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/middleware" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/org" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/setting" ) type URLPrefs struct { Language string Theme string } // URL prefs take precedence over any saved user preferences func getURLPrefs(c *contextmodel.ReqContext) URLPrefs { language := c.Query("lang") theme := c.Query("theme") return URLPrefs{ Language: language, Theme: theme, } } func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexViewData, error) { c, span := hs.injectSpan(c, "api.setIndexViewData") defer span.End() settings, err := hs.getFrontendSettings(c) if err != nil { return nil, err } userID, _ := identity.UserIdentifier(c.GetID()) c, prefsSpan := hs.injectSpan(c, "api.setIndexViewData.preferences") var prefs *pref.Preference if ofClient.Boolean(c.Req.Context(), featuremgmt.FlagPreferencesRerouteLegacyAPIs, false, openfeature.TransactionContext(c.Req.Context())) { prefs, err = hs.preferenceK8sHandler.GetPreferencesWithDefaults(c) } else { prefsQuery := pref.GetPreferenceWithDefaultsQuery{ UserID: userID, OrgID: c.GetOrgID(), Teams: c.TeamIDs, // nolint:staticcheck } prefs, err = hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery) } prefsSpan.End() if err != nil { return nil, err } // Locale is used for some number and date/time formatting, whereas language is used just for // translating words in the interface acceptLangHeader := c.Req.Header.Get("Accept-Language") acceptLangHeaderFirstValue := "" if len(acceptLangHeader) > 0 { parts := strings.Split(acceptLangHeader, ",") acceptLangHeaderFirstValue = parts[0] } locale := "en-US" // default to en formatting, but use the accept-lang header or user's preference if acceptLangHeaderFirstValue != "" { locale = acceptLangHeaderFirstValue } language := "" // frontend will set the default language urlPrefs := getURLPrefs(c) if urlPrefs.Language != "" { language = urlPrefs.Language } else if prefs.JSONData.Language != "" { language = prefs.JSONData.Language } appURL := hs.Cfg.AppURL appSubURL := hs.Cfg.AppSubURL // special case when doing localhost call from image renderer if c.IsRenderCall && !hs.Cfg.ServeFromSubPath { appURL = fmt.Sprintf("%s://localhost:%s", hs.Cfg.Protocol, hs.Cfg.HTTPPort) appSubURL = "" settings.AppSubUrl = "" } ctx := c.Req.Context() renderBindingSupported, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagReportRenderBinding, false, openfeature.TransactionContext(ctx)) useLuxon, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagDatetimeUseLuxon, false, openfeature.TransactionContext(ctx)) grafanaAssetSriChecks, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagGrafanaAssetSriChecks, false, openfeature.TransactionContext(ctx)) newPreferencesPage, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagGrafanaNewPreferencesPage, false, openfeature.TransactionContext(ctx)) ofrepRootUrlEnabled := ofClient.Boolean(ctx, featuremgmt.FlagGrafanaOfrepRootUrl, false, openfeature.TransactionContext(ctx)) // With the client-built nav tree the frontend only needs the items it cannot // know about (enterprise index-data hooks add theirs to the empty root below, // and the client grafts them in) — skip building the full tree. The // conditions must mirror isClientNavTreeEnabled (buildStaticNavTree.ts): // the frontend falls back to the bootdata tree unless both flags are // enabled, so skipping on a different set here would leave it with an // empty tree. multiTenantNavTree, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagGrafanaMultiTenantNavTree, false, openfeature.TransactionContext(ctx)) useMTPlugins, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagPluginsUseMTPlugins, false, openfeature.TransactionContext(ctx)) clientNavTree := multiTenantNavTree && useMTPlugins // Children must be non-nil so bootdata serves an empty array rather than // null: frontends that read bootData.navTree directly crash on null. navTree := &navtree.NavTreeRoot{Children: []*navtree.NavLink{}} if !clientNavTree { var err error navTree, err = hs.navTreeService.GetNavTree(c, prefs) if err != nil { return nil, err } } weekStart := "" if prefs.WeekStart != nil { weekStart = *prefs.WeekStart } theme := hs.getThemeForIndexData(prefs.Theme, urlPrefs.Theme) assets, err := webassets.GetWebAssets(c.Req.Context(), "build", hs.Cfg, hs.License) if err != nil { return nil, err } hasAccess := ac.HasAccess(hs.AccessControl, c) hasEditPerm := hasAccess(ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(folder.ActionFoldersCreate))) data := dtos.IndexViewData{ User: &dtos.CurrentUser{ Id: userID, UID: c.UserUID, // << not set yet IsSignedIn: c.IsSignedIn, Login: c.Login, Email: c.GetEmail(), Name: c.Name, OrgId: c.GetOrgID(), OrgName: c.OrgName, OrgRole: c.GetOrgRole(), OrgCount: hs.getUserOrgCount(c, userID), GravatarUrl: dtos.GetGravatarUrl(hs.Cfg, c.GetEmail()), IsGrafanaAdmin: c.IsGrafanaAdmin, Theme: theme.ID, LightTheme: theme.Type == "light", Timezone: prefs.Timezone, WeekStart: weekStart, Locale: locale, Language: language, HasEditPermissionInFolders: hasEditPerm, Analytics: hs.buildUserAnalyticsSettings(c), AuthenticatedBy: c.GetAuthenticatedBy(), }, Settings: settings, ThemeType: theme.Type, AppUrl: appURL, AppSubUrl: appSubURL, NewsFeedEnabled: hs.Cfg.NewsFeedEnabled, GoogleAnalyticsId: settings.GoogleAnalyticsId, GoogleAnalytics4Id: settings.GoogleAnalytics4Id, GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews, GoogleTagManagerId: hs.Cfg.GoogleTagManagerID, BuildVersion: setting.BuildVersion, BuildCommit: setting.BuildCommit, NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(), NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(), AppName: setting.ApplicationName, AppNameBodyClass: "app-grafana", FavIcon: template.URL(assets.ContentDeliveryURL + "public/build/img/fav32.png"), // #nosec G203 AppleTouchIcon: template.URL(assets.ContentDeliveryURL + "public/build/img/apple-touch-icon.png"), // #nosec G203 AppTitle: "Grafana", NavTree: navTree, Nonce: c.RequestNonce, LoadingLogo: template.URL(assets.ContentDeliveryURL + "public/build/img/grafana_icon.svg"), // #nosec G203 IsDevelopmentEnv: hs.Cfg.Env == setting.Dev, Assets: assets, RenderBindingSupported: renderBindingSupported, UseLuxon: useLuxon, AssetSriChecksEnabled: grafanaAssetSriChecks, NewPreferencesPage: newPreferencesPage, OFREPRootUrlEnabled: ofrepRootUrlEnabled, } if hs.Cfg.CSPEnabled { data.CSPEnabled = true hosts := middleware.CSPHostLists{FormActionAdditionalHosts: hs.Cfg.FormActionAdditionalHosts} data.CSPContent = middleware.ReplacePolicyVariables(hs.Cfg.CSPTemplate, appURL, hosts, c.RequestNonce) } userPermissions, err := hs.accesscontrolService.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: false}) if err != nil { return nil, err } data.User.Permissions = ac.BuildPermissionsMap(userPermissions) if hs.Cfg.DisableGravatar { data.User.GravatarUrl = hs.Cfg.AppSubURL + "/public/img/user_profile.png" } if len(data.User.Name) == 0 { data.User.Name = data.User.Login } hs.HooksService.RunIndexDataHooks(&data, c) data.NavTree.RemoveEmptyAdminSections() data.NavTree.RemoveEmptyConnectionsSection() data.NavTree.Sort() return &data, nil } func (hs *HTTPServer) buildUserAnalyticsSettings(c *contextmodel.ReqContext) dtos.AnalyticsSettings { // Anonymous users do not have an email or auth info if !c.IsIdentityType(claims.TypeUser) { return dtos.AnalyticsSettings{Identifier: "@" + hs.Cfg.AppURL} } if !c.IsSignedIn { return dtos.AnalyticsSettings{} } identifier := c.GetEmail() + "@" + hs.Cfg.AppURL if authenticatedBy := c.GetAuthenticatedBy(); authenticatedBy == login.GrafanaComAuthModule { identifier = c.GetAuthID() } return dtos.AnalyticsSettings{ Identifier: identifier, IntercomIdentifier: hashUserIdentifier(identifier, hs.Cfg.IntercomSecret), } } func (hs *HTTPServer) getUserOrgCount(c *contextmodel.ReqContext, userID int64) int { if userID == 0 { return 1 } c, span := hs.injectSpan(c, "api.getUserOrgCount") defer span.End() userOrgs, err := hs.orgService.GetUserOrgList(c.Req.Context(), &org.GetUserOrgListQuery{UserID: userID}) if err != nil { hs.log.FromContext(c.Req.Context()).Error("Failed to count user orgs", "userId", userID, "error", err) return 1 } return len(userOrgs) } func hashUserIdentifier(identifier string, secret string) string { if secret == "" { return "" } key := []byte(secret) h := hmac.New(sha256.New, key) h.Write([]byte(identifier)) return hex.EncodeToString(h.Sum(nil)) } func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { c, span := hs.injectSpan(c, "api.Index") defer span.End() start := time.Now() defer func() { metricutil.ObserveWithExemplar(c.Req.Context(), hs.htmlHandlerRequestsDuration.WithLabelValues("index"), time.Since(start).Seconds()) }() data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) return } c.HTML(http.StatusOK, "index", data) } func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(http.StatusNotFound, "Not found", nil) return } start := time.Now() defer func() { metricutil.ObserveWithExemplar(c.Req.Context(), hs.htmlHandlerRequestsDuration.WithLabelValues("not_found"), time.Since(start).Seconds()) }() data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) return } c.HTML(http.StatusNotFound, "index", data) } func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam string) *pref.ThemeDTO { if themeURLParam != "" && pref.IsValidThemeID(themeURLParam) { return pref.GetThemeByID(themeURLParam) } if pref.IsValidThemeID(themePrefId) { return pref.GetThemeByID(themePrefId) } return pref.GetThemeByID(hs.Cfg.DefaultTheme) }