// Copyright 2014 Unknwon // Copyright 2014 Torkel Ödegaard package setting import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util" "github.com/gobwas/glob" "github.com/prometheus/common/model" "gopkg.in/ini.v1" ) type Scheme string const ( HTTPScheme Scheme = "http" HTTPSScheme Scheme = "https" HTTP2Scheme Scheme = "h2" SocketScheme Scheme = "socket" ) const ( RedactedPassword = "*********" DefaultHTTPAddr = "0.0.0.0" Dev = "development" Prod = "production" Test = "test" ApplicationName = "Grafana" ) // zoneInfo names environment variable for setting the path to look for the timezone database in go const zoneInfo = "ZONEINFO" var ( // App settings. Env = Dev AppUrl string AppSubUrl string ServeFromSubPath bool InstanceName string // build BuildVersion string BuildCommit string BuildBranch string BuildStamp int64 IsEnterprise bool // packaging Packaging = "unknown" // Paths HomePath string CustomInitPath = "conf/custom.ini" // HTTP server options StaticRootPath string // Security settings. SecretKey string DisableGravatar bool DataProxyWhiteList map[string]bool CookieSecure bool CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite // Snapshots ExternalSnapshotUrl string ExternalSnapshotName string ExternalEnabled bool SnapShotRemoveExpired bool // Dashboard history DashboardVersionsToKeep int MinRefreshInterval string // User settings AllowUserSignUp bool AllowUserOrgCreate bool AutoAssignOrg bool AutoAssignOrgId int AutoAssignOrgRole string VerifyEmailEnabled bool LoginHint string PasswordHint string DisableLoginForm bool DisableSignoutMenu bool SignoutRedirectUrl string ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string OAuthAutoLogin bool ViewersCanEdit bool // HTTP auth SigV4AuthEnabled bool AzureAuthEnabled bool AnonymousEnabled bool // Auth proxy settings AuthProxyEnabled bool AuthProxyHeaderProperty string // Basic Auth BasicAuthEnabled bool // Global setting objects. Raw *ini.File // for logging purposes configFiles []string appliedCommandLineProperties []string appliedEnvOverrides []string // analytics GoogleAnalyticsId string GoogleTagManagerId string RudderstackDataPlaneUrl string RudderstackWriteKey string RudderstackSdkUrl string RudderstackConfigUrl string // LDAP LDAPEnabled bool LDAPConfigFile string LDAPSyncCron string LDAPAllowSignup bool LDAPActiveSyncEnabled bool // Quota Quota QuotaSettings // Alerting AlertingEnabled *bool ExecuteAlerts bool AlertingRenderLimit int AlertingErrorOrTimeout string AlertingNoDataOrNullValues string AlertingEvaluationTimeout time.Duration AlertingNotificationTimeout time.Duration AlertingMaxAttempts int AlertingMinInterval int64 // Explore UI ExploreEnabled bool // Help UI HelpEnabled bool // Profile UI ProfileEnabled bool // Grafana.NET URL GrafanaComUrl string ImageUploadProvider string ) // AddChangePasswordLink returns if login form is disabled or not since // the same intention can be used to hide both features. func AddChangePasswordLink() bool { return !DisableLoginForm } // TODO move all global vars to this struct type Cfg struct { Raw *ini.File Logger log.Logger // HTTP Server Settings CertFile string KeyFile string HTTPAddr string HTTPPort string AppURL string AppSubURL string ServeFromSubPath bool StaticRootPath string Protocol Scheme SocketPath string RouterLogging bool Domain string CDNRootURL *url.URL ReadTimeout time.Duration EnableGzip bool EnforceDomain bool // Security settings SecretKey string EmailCodeValidMinutes int // build BuildVersion string BuildCommit string BuildBranch string BuildStamp int64 IsEnterprise bool // packaging Packaging string // Paths HomePath string ProvisioningPath string DataPath string LogsPath string PluginsPath string BundledPluginsPath string // SMTP email settings Smtp SmtpSettings // Rendering ImagesDir string CSVsDir string RendererUrl string RendererCallbackUrl string RendererConcurrentRequestLimit int // Security DisableInitAdminCreation bool DisableBruteForceLoginProtection bool CookieSecure bool CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite AllowEmbedding bool XSSProtectionHeader bool ContentTypeProtectionHeader bool StrictTransportSecurity bool StrictTransportSecurityMaxAge int StrictTransportSecurityPreload bool StrictTransportSecuritySubDomains bool // CSPEnabled toggles Content Security Policy support. CSPEnabled bool // CSPTemplate contains the Content Security Policy template. CSPTemplate string AngularSupportEnabled bool TempDataLifetime time.Duration PluginsEnableAlpha bool PluginsAppsSkipVerifyTLS bool PluginSettings PluginSettings PluginsAllowUnsigned []string PluginCatalogURL string PluginCatalogHiddenPlugins []string PluginAdminEnabled bool PluginAdminExternalManageEnabled bool DisableSanitizeHtml bool EnterpriseLicensePath string // Metrics MetricsEndpointEnabled bool MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthPassword string MetricsEndpointDisableTotalStats bool MetricsGrafanaEnvironmentInfo map[string]string // Dashboards DefaultHomeDashboardPath string // Auth LoginCookieName string LoginMaxInactiveLifetime time.Duration LoginMaxLifetime time.Duration TokenRotationIntervalMinutes int SigV4AuthEnabled bool SigV4VerboseLogging bool AzureAuthEnabled bool BasicAuthEnabled bool AdminUser string AdminPassword string // AWS Plugin Auth AWSAllowedAuthProviders []string AWSAssumeRoleEnabled bool AWSListMetricsPageLimit int // Azure Cloud settings Azure *azsettings.AzureSettings // Auth proxy settings AuthProxyEnabled bool AuthProxyHeaderName string AuthProxyHeaderProperty string AuthProxyAutoSignUp bool AuthProxyEnableLoginToken bool AuthProxyWhitelist string AuthProxyHeaders map[string]string AuthProxyHeadersEncoded bool AuthProxySyncTTL int // OAuth OAuthCookieMaxAge int // JWT Auth JWTAuthEnabled bool JWTAuthHeaderName string JWTAuthURLLogin bool JWTAuthEmailClaim string JWTAuthUsernameClaim string JWTAuthExpectClaims string JWTAuthJWKSetURL string JWTAuthCacheTTL time.Duration JWTAuthKeyFile string JWTAuthJWKSetFile string JWTAuthAutoSignUp bool // Dataproxy SendUserHeader bool DataProxyLogging bool DataProxyTimeout int DataProxyDialTimeout int DataProxyTLSHandshakeTimeout int DataProxyExpectContinueTimeout int DataProxyMaxConnsPerHost int DataProxyMaxIdleConns int DataProxyKeepAlive int DataProxyIdleConnTimeout int ResponseLimit int64 DataProxyRowLimit int64 // DistributedCache RemoteCacheOptions *RemoteCacheOptions EditorsCanAdmin bool ApiKeyMaxSecondsToLive int64 // Check if a feature toggle is enabled // @deprecated IsFeatureToggleEnabled func(key string) bool // filled in dynamically AnonymousEnabled bool AnonymousOrgName string AnonymousOrgRole string AnonymousHideVersion bool DateFormats DateFormats // User UserInviteMaxLifetime time.Duration HiddenUsers map[string]struct{} CaseInsensitiveLogin bool // Login and Email will be considered case insensitive // Annotations AnnotationCleanupJobBatchSize int64 AlertingAnnotationCleanupSetting AnnotationCleanupSettings DashboardAnnotationCleanupSettings AnnotationCleanupSettings APIAnnotationCleanupSettings AnnotationCleanupSettings // Sentry config Sentry Sentry // GrafanaJavascriptAgent config GrafanaJavascriptAgent GrafanaJavascriptAgent // Data sources DataSourceLimit int // Snapshots SnapshotPublicMode bool ErrTemplateName string Env string ForceMigration bool // Analytics CheckForGrafanaUpdates bool CheckForPluginUpdates bool ReportingDistributor string ReportingEnabled bool ApplicationInsightsConnectionString string ApplicationInsightsEndpointUrl string FeedbackLinksEnabled bool // LDAP LDAPEnabled bool LDAPAllowSignup bool Quota QuotaSettings DefaultTheme string DefaultLocale string HomePage string AutoAssignOrg bool AutoAssignOrgId int AutoAssignOrgRole string OAuthSkipOrgRoleUpdateSync bool // ExpressionsEnabled specifies whether expressions are enabled. ExpressionsEnabled bool ImageUploadProvider string // LiveMaxConnections is a maximum number of WebSocket connections to // Grafana Live ws endpoint (per Grafana server instance). 0 disables // Live, -1 means unlimited connections. LiveMaxConnections int // LiveHAEngine is a type of engine to use to achieve HA with Grafana Live. // Zero value means in-memory single node setup. LiveHAEngine string // LiveHAEngineAddress is a connection address for Live HA engine. LiveHAEngineAddress string // LiveAllowedOrigins is a set of origins accepted by Live. If not provided // then Live uses AppURL as the only allowed origin. LiveAllowedOrigins []string // Grafana.com URL GrafanaComURL string // Geomap base layer config GeomapDefaultBaseLayerConfig map[string]interface{} GeomapEnableCustomBaseLayers bool // Unified Alerting UnifiedAlerting UnifiedAlertingSettings // Query history QueryHistoryEnabled bool DashboardPreviews DashboardPreviewsSettings Storage StorageSettings // Access Control RBACEnabled bool RBACPermissionCache bool // Undocumented option as a backup in case removing builtin-role assignment // fails RBACBuiltInRoleAssignmentEnabled bool } type CommandLineArgs struct { Config string HomePath string Args []string } func (cfg Cfg) parseAppUrlAndSubUrl(section *ini.Section) (string, string, error) { appUrl := valueAsString(section, "root_url", "http://localhost:3000/") if appUrl[len(appUrl)-1] != '/' { appUrl += "/" } // Check if has app suburl. url, err := url.Parse(appUrl) if err != nil { cfg.Logger.Error("Invalid root_url.", "url", appUrl, "error", err) os.Exit(1) } appSubUrl := strings.TrimSuffix(url.Path, "/") return appUrl, appSubUrl, nil } func ToAbsUrl(relativeUrl string) string { return AppUrl + relativeUrl } func RedactedValue(key, value string) string { uppercased := strings.ToUpper(key) // Sensitive information: password, secrets etc for _, pattern := range []string{ "PASSWORD", "SECRET", "PROVIDER_CONFIG", "PRIVATE_KEY", "SECRET_KEY", "CERTIFICATE", "ACCOUNT_KEY", "ENCRYPTION_KEY", "VAULT_TOKEN", } { if match, err := regexp.MatchString(pattern, uppercased); match && err == nil { return RedactedPassword } } for _, exception := range []string{ "RUDDERSTACK", "APPLICATION_INSIGHTS", "SENTRY", } { if strings.Contains(uppercased, exception) { return value } } if u, err := RedactedURL(value); err == nil { return u } return value } func RedactedURL(value string) (string, error) { // Value could be a list of URLs chunks := util.SplitString(value) for i, chunk := range chunks { var hasTmpPrefix bool const tmpPrefix = "http://" if !strings.Contains(chunk, "://") { chunk = tmpPrefix + chunk hasTmpPrefix = true } u, err := url.Parse(chunk) if err != nil { return "", err } redacted := u.Redacted() if hasTmpPrefix { redacted = strings.Replace(redacted, tmpPrefix, "", 1) } chunks[i] = redacted } if strings.Contains(value, ",") { return strings.Join(chunks, ","), nil } return strings.Join(chunks, " "), nil } func applyEnvVariableOverrides(file *ini.File) error { appliedEnvOverrides = make([]string, 0) for _, section := range file.Sections() { for _, key := range section.Keys() { envKey := EnvKey(section.Name(), key.Name()) envValue := os.Getenv(envKey) if len(envValue) > 0 { key.SetValue(envValue) appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue))) } } } return nil } func (cfg *Cfg) readGrafanaEnvironmentMetrics() error { environmentMetricsSection := cfg.Raw.Section("metrics.environment_info") keys := environmentMetricsSection.Keys() cfg.MetricsGrafanaEnvironmentInfo = make(map[string]string, len(keys)) for _, key := range keys { labelName := model.LabelName(key.Name()) labelValue := model.LabelValue(key.Value()) if !labelName.IsValid() { return fmt.Errorf("invalid label name in [metrics.environment_info] configuration. name %q", labelName) } if !labelValue.IsValid() { return fmt.Errorf("invalid label value in [metrics.environment_info] configuration. name %q value %q", labelName, labelValue) } cfg.MetricsGrafanaEnvironmentInfo[string(labelName)] = string(labelValue) } return nil } func (cfg *Cfg) readAnnotationSettings() { section := cfg.Raw.Section("annotations") cfg.AnnotationCleanupJobBatchSize = section.Key("cleanupjob_batchsize").MustInt64(100) dashboardAnnotation := cfg.Raw.Section("annotations.dashboard") apiIAnnotation := cfg.Raw.Section("annotations.api") alertingSection := cfg.Raw.Section("alerting") var newAnnotationCleanupSettings = func(section *ini.Section, maxAgeField string) AnnotationCleanupSettings { maxAge, err := gtime.ParseDuration(section.Key(maxAgeField).MustString("")) if err != nil { maxAge = 0 } return AnnotationCleanupSettings{ MaxAge: maxAge, MaxCount: section.Key("max_annotations_to_keep").MustInt64(0), } } cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingSection, "max_annotation_age") cfg.DashboardAnnotationCleanupSettings = newAnnotationCleanupSettings(dashboardAnnotation, "max_age") cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age") } func (cfg *Cfg) readExpressionsSettings() { expressions := cfg.Raw.Section("expressions") cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true) } type AnnotationCleanupSettings struct { MaxAge time.Duration MaxCount int64 } func EnvKey(sectionName string, keyName string) string { sN := strings.ToUpper(strings.ReplaceAll(sectionName, ".", "_")) sN = strings.ReplaceAll(sN, "-", "_") kN := strings.ToUpper(strings.ReplaceAll(keyName, ".", "_")) envKey := fmt.Sprintf("GF_%s_%s", sN, kN) return envKey } func applyCommandLineDefaultProperties(props map[string]string, file *ini.File) { appliedCommandLineProperties = make([]string, 0) for _, section := range file.Sections() { for _, key := range section.Keys() { keyString := fmt.Sprintf("default.%s.%s", section.Name(), key.Name()) value, exists := props[keyString] if exists { key.SetValue(value) appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, RedactedValue(keyString, value))) } } } } func applyCommandLineProperties(props map[string]string, file *ini.File) { for _, section := range file.Sections() { sectionName := section.Name() + "." if section.Name() == ini.DefaultSection { sectionName = "" } for _, key := range section.Keys() { keyString := sectionName + key.Name() value, exists := props[keyString] if exists { appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value)) key.SetValue(value) } } } } func (cfg Cfg) getCommandLineProperties(args []string) map[string]string { props := make(map[string]string) for _, arg := range args { if !strings.HasPrefix(arg, "cfg:") { continue } trimmed := strings.TrimPrefix(arg, "cfg:") parts := strings.Split(trimmed, "=") if len(parts) != 2 { cfg.Logger.Error("Invalid command line argument.", "argument", arg) os.Exit(1) } props[parts[0]] = parts[1] } return props } func makeAbsolute(path string, root string) string { if filepath.IsAbs(path) { return path } return filepath.Join(root, path) } func (cfg *Cfg) loadSpecifiedConfigFile(configFile string, masterFile *ini.File) error { if configFile == "" { configFile = filepath.Join(cfg.HomePath, CustomInitPath) // return without error if custom file does not exist if !pathExists(configFile) { return nil } } userConfig, err := ini.Load(configFile) if err != nil { return fmt.Errorf("failed to parse %q: %w", configFile, err) } userConfig.BlockMode = false for _, section := range userConfig.Sections() { for _, key := range section.Keys() { if key.Value() == "" { continue } defaultSec, err := masterFile.GetSection(section.Name()) if err != nil { defaultSec, _ = masterFile.NewSection(section.Name()) } defaultKey, err := defaultSec.GetKey(key.Name()) if err != nil { defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value()) } defaultKey.SetValue(key.Value()) } } configFiles = append(configFiles, configFile) return nil } func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) { // load config defaults defaultConfigFile := path.Join(HomePath, "conf/defaults.ini") configFiles = append(configFiles, defaultConfigFile) // check if config file exists if _, err := os.Stat(defaultConfigFile); os.IsNotExist(err) { fmt.Println("Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepath") os.Exit(1) } // load defaults parsedFile, err := ini.Load(defaultConfigFile) if err != nil { fmt.Printf("Failed to parse defaults.ini, %v\n", err) os.Exit(1) return nil, err } parsedFile.BlockMode = false // command line props commandLineProps := cfg.getCommandLineProperties(args.Args) // load default overrides applyCommandLineDefaultProperties(commandLineProps, parsedFile) // load specified config file err = cfg.loadSpecifiedConfigFile(args.Config, parsedFile) if err != nil { err2 := cfg.initLogging(parsedFile) if err2 != nil { return nil, err2 } cfg.Logger.Error(err.Error()) os.Exit(1) } // apply environment overrides err = applyEnvVariableOverrides(parsedFile) if err != nil { return nil, err } // apply command line overrides applyCommandLineProperties(commandLineProps, parsedFile) // evaluate config values containing environment variables err = expandConfig(parsedFile) if err != nil { return nil, err } // update data path and logging config dataPath := valueAsString(parsedFile.Section("paths"), "data", "") cfg.DataPath = makeAbsolute(dataPath, HomePath) err = cfg.initLogging(parsedFile) if err != nil { return nil, err } cfg.Logger.Info(fmt.Sprintf("Starting %s", ApplicationName), "version", BuildVersion, "commit", BuildCommit, "branch", BuildBranch, "compiled", time.Unix(BuildStamp, 0)) return parsedFile, err } func pathExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } if os.IsNotExist(err) { return false } return false } func (cfg *Cfg) setHomePath(args CommandLineArgs) { if args.HomePath != "" { cfg.HomePath = args.HomePath HomePath = cfg.HomePath return } var err error cfg.HomePath, err = filepath.Abs(".") if err != nil { panic(err) } HomePath = cfg.HomePath // check if homepath is correct if pathExists(filepath.Join(cfg.HomePath, "conf/defaults.ini")) { return } // try down one path if pathExists(filepath.Join(cfg.HomePath, "../conf/defaults.ini")) { cfg.HomePath = filepath.Join(cfg.HomePath, "../") HomePath = cfg.HomePath } } var skipStaticRootValidation = false func NewCfg() *Cfg { return &Cfg{ Logger: log.New("settings"), Raw: ini.Empty(), Azure: &azsettings.AzureSettings{}, RBACEnabled: true, } } func NewCfgFromArgs(args CommandLineArgs) (*Cfg, error) { cfg := NewCfg() if err := cfg.Load(args); err != nil { return nil, err } return cfg, nil } func (cfg *Cfg) validateStaticRootPath() error { if skipStaticRootValidation { return nil } if _, err := os.Stat(path.Join(StaticRootPath, "build")); err != nil { cfg.Logger.Error("Failed to detect generated javascript files in public/build") } return nil } func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.setHomePath(args) // Fix for missing IANA db on Windows _, zoneInfoSet := os.LookupEnv(zoneInfo) if runtime.GOOS == "windows" && !zoneInfoSet { if err := os.Setenv(zoneInfo, filepath.Join(HomePath, "tools", "zoneinfo.zip")); err != nil { cfg.Logger.Error("Can't set ZONEINFO environment variable", "err", err) } } iniFile, err := cfg.loadConfiguration(args) if err != nil { return err } cfg.Raw = iniFile // Temporarily keep global, to make refactor in steps Raw = cfg.Raw cfg.BuildVersion = BuildVersion cfg.BuildCommit = BuildCommit cfg.BuildStamp = BuildStamp cfg.BuildBranch = BuildBranch cfg.IsEnterprise = IsEnterprise cfg.Packaging = Packaging cfg.ErrTemplateName = "error" Env = valueAsString(iniFile.Section(""), "app_mode", "development") cfg.Env = Env cfg.ForceMigration = iniFile.Section("").Key("force_migration").MustBool(false) InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name") plugins := valueAsString(iniFile.Section("paths"), "plugins", "") cfg.PluginsPath = makeAbsolute(plugins, HomePath) cfg.BundledPluginsPath = makeAbsolute("plugins-bundled", HomePath) provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath) if err := cfg.readServerSettings(iniFile); err != nil { return err } if err := readDataProxySettings(iniFile, cfg); err != nil { return err } if err := readSecuritySettings(iniFile, cfg); err != nil { return err } if err := readSnapshotsSettings(cfg, iniFile); err != nil { return err } // read dashboard settings dashboards := iniFile.Section("dashboards") DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") if err := readUserSettings(iniFile, cfg); err != nil { return err } if err := readAuthSettings(iniFile, cfg); err != nil { return err } readAccessControlSettings(iniFile, cfg) if err := cfg.readRenderingSettings(iniFile); err != nil { return err } cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) cfg.MetricsEndpointBasicAuthUsername = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "") cfg.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "") cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false) analytics := iniFile.Section("analytics") cfg.CheckForGrafanaUpdates = analytics.Key("check_for_updates").MustBool(true) cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true) GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() RudderstackWriteKey = analytics.Key("rudderstack_write_key").String() RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String() RudderstackSdkUrl = analytics.Key("rudderstack_sdk_url").String() RudderstackConfigUrl = analytics.Key("rudderstack_config_url").String() cfg.ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) cfg.ReportingDistributor = analytics.Key("reporting_distributor").MustString("grafana-labs") if len(cfg.ReportingDistributor) >= 100 { cfg.ReportingDistributor = cfg.ReportingDistributor[:100] } cfg.ApplicationInsightsConnectionString = analytics.Key("application_insights_connection_string").String() cfg.ApplicationInsightsEndpointUrl = analytics.Key("application_insights_endpoint_url").String() cfg.FeedbackLinksEnabled = analytics.Key("feedback_links_enabled").MustBool(true) if err := readAlertingSettings(iniFile); err != nil { return err } explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(true) help := iniFile.Section("help") HelpEnabled = help.Key("enabled").MustBool(true) profile := iniFile.Section("profile") ProfileEnabled = profile.Key("enabled").MustBool(true) queryHistory := iniFile.Section("query_history") cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) panelsSection := iniFile.Section("panels") cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false) if err := cfg.readPluginSettings(iniFile); err != nil { return err } if err := cfg.readFeatureToggles(iniFile); err != nil { return err } if err := cfg.ReadUnifiedAlertingSettings(iniFile); err != nil { return err } // check old location for this option if panelsSection.Key("enable_alpha").MustBool(false) { cfg.PluginsEnableAlpha = true } cfg.readLDAPConfig() cfg.handleAWSConfig() cfg.readAzureSettings() cfg.readSessionConfig() cfg.readSmtpSettings() cfg.readQuotaSettings() cfg.readAnnotationSettings() cfg.readExpressionsSettings() if err := cfg.readGrafanaEnvironmentMetrics(); err != nil { return err } cfg.readDataSourcesSettings() cfg.DashboardPreviews = readDashboardPreviewsSettings(iniFile) cfg.Storage = readStorageSettings(iniFile) if VerifyEmailEnabled && !cfg.Smtp.Enabled { cfg.Logger.Warn("require_email_validation is enabled but smtp is disabled") } // check old key name GrafanaComUrl = valueAsString(iniFile.Section("grafana_net"), "url", "") if GrafanaComUrl == "" { GrafanaComUrl = valueAsString(iniFile.Section("grafana_com"), "url", "https://grafana.com") } cfg.GrafanaComURL = GrafanaComUrl imageUploadingSection := iniFile.Section("external_image_storage") cfg.ImageUploadProvider = valueAsString(imageUploadingSection, "provider", "") ImageUploadProvider = cfg.ImageUploadProvider enterprise := iniFile.Section("enterprise") cfg.EnterpriseLicensePath = valueAsString(enterprise, "license_path", filepath.Join(cfg.DataPath, "license.jwt")) cacheServer := iniFile.Section("remote_cache") dbName := valueAsString(cacheServer, "type", "database") connStr := valueAsString(cacheServer, "connstr", "") cfg.RemoteCacheOptions = &RemoteCacheOptions{ Name: dbName, ConnStr: connStr, } geomapSection := iniFile.Section("geomap") basemapJSON := valueAsString(geomapSection, "default_baselayer_config", "") if basemapJSON != "" { layer := make(map[string]interface{}) err = json.Unmarshal([]byte(basemapJSON), &layer) if err != nil { cfg.Logger.Error("Error reading json from default_baselayer_config", "error", err) } else { cfg.GeomapDefaultBaseLayerConfig = layer } } cfg.GeomapEnableCustomBaseLayers = geomapSection.Key("enable_custom_baselayers").MustBool(true) cfg.readDateFormats() cfg.readSentryConfig() cfg.readGrafanaJavascriptAgentConfig() if err := cfg.readLiveSettings(iniFile); err != nil { return err } cfg.LogConfigSources() return nil } func valueAsString(section *ini.Section, keyName string, defaultValue string) string { return section.Key(keyName).MustString(defaultValue) } type RemoteCacheOptions struct { Name string ConnStr string } func (cfg *Cfg) readLDAPConfig() { ldapSec := cfg.Raw.Section("auth.ldap") LDAPConfigFile = ldapSec.Key("config_file").String() LDAPSyncCron = ldapSec.Key("sync_cron").String() LDAPEnabled = ldapSec.Key("enabled").MustBool(false) cfg.LDAPEnabled = LDAPEnabled LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) cfg.LDAPAllowSignup = LDAPAllowSignup } func (cfg *Cfg) handleAWSConfig() { awsPluginSec := cfg.Raw.Section("aws") cfg.AWSAssumeRoleEnabled = awsPluginSec.Key("assume_role_enabled").MustBool(true) allowedAuthProviders := awsPluginSec.Key("allowed_auth_providers").MustString("default,keys,credentials") for _, authProvider := range strings.Split(allowedAuthProviders, ",") { authProvider = strings.TrimSpace(authProvider) if authProvider != "" { cfg.AWSAllowedAuthProviders = append(cfg.AWSAllowedAuthProviders, authProvider) } } cfg.AWSListMetricsPageLimit = awsPluginSec.Key("list_metrics_page_limit").MustInt(500) // Also set environment variables that can be used by core plugins err := os.Setenv(awsds.AssumeRoleEnabledEnvVarKeyName, strconv.FormatBool(cfg.AWSAssumeRoleEnabled)) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AssumeRoleEnabledEnvVarKeyName), err) } err = os.Setenv(awsds.AllowedAuthProvidersEnvVarKeyName, allowedAuthProviders) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AllowedAuthProvidersEnvVarKeyName), err) } } func (cfg *Cfg) readSessionConfig() { sec, _ := cfg.Raw.GetSection("session") if sec != nil { cfg.Logger.Warn( "[Removed] Session setting was removed in v6.2, use remote_cache option instead", ) } } func (cfg *Cfg) initLogging(file *ini.File) error { logModeStr := valueAsString(file.Section("log"), "mode", "console") // split on comma logModes := strings.Split(logModeStr, ",") // also try space if len(logModes) == 1 { logModes = strings.Split(logModeStr, " ") } logsPath := valueAsString(file.Section("paths"), "logs", "") cfg.LogsPath = makeAbsolute(logsPath, HomePath) return log.ReadLoggingConfig(logModes, cfg.LogsPath, file) } func (cfg *Cfg) LogConfigSources() { var text bytes.Buffer for _, file := range configFiles { cfg.Logger.Info("Config loaded from", "file", file) } if len(appliedCommandLineProperties) > 0 { for _, prop := range appliedCommandLineProperties { cfg.Logger.Info("Config overridden from command line", "arg", prop) } } if len(appliedEnvOverrides) > 0 { text.WriteString("\tEnvironment variables used:\n") for _, prop := range appliedEnvOverrides { cfg.Logger.Info("Config overridden from Environment variable", "var", prop) } } cfg.Logger.Info("Path Home", "path", HomePath) cfg.Logger.Info("Path Data", "path", cfg.DataPath) cfg.Logger.Info("Path Logs", "path", cfg.LogsPath) cfg.Logger.Info("Path Plugins", "path", cfg.PluginsPath) cfg.Logger.Info("Path Provisioning", "path", cfg.ProvisioningPath) cfg.Logger.Info("App mode " + cfg.Env) } type DynamicSection struct { section *ini.Section Logger log.Logger } // Key dynamically overrides keys with environment variables. // As a side effect, the value of the setting key will be updated if an environment variable is present. func (s *DynamicSection) Key(k string) *ini.Key { envKey := EnvKey(s.section.Name(), k) envValue := os.Getenv(envKey) key := s.section.Key(k) if len(envValue) == 0 { return key } key.SetValue(envValue) s.Logger.Info("Config overridden from Environment variable", "var", fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue))) return key } // SectionWithEnvOverrides dynamically overrides keys with environment variables. // As a side effect, the value of the setting key will be updated if an environment variable is present. func (cfg *Cfg) SectionWithEnvOverrides(s string) *DynamicSection { return &DynamicSection{cfg.Raw.Section(s), cfg.Logger} } func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { security := iniFile.Section("security") SecretKey = valueAsString(security, "secret_key", "") cfg.SecretKey = SecretKey DisableGravatar = security.Key("disable_gravatar").MustBool(true) cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) CookieSecure = security.Key("cookie_secure").MustBool(false) cfg.CookieSecure = CookieSecure samesiteString := valueAsString(security, "cookie_samesite", "lax") if samesiteString == "disabled" { CookieSameSiteDisabled = true cfg.CookieSameSiteDisabled = CookieSameSiteDisabled } else { validSameSiteValues := map[string]http.SameSite{ "lax": http.SameSiteLaxMode, "strict": http.SameSiteStrictMode, "none": http.SameSiteNoneMode, } if samesite, ok := validSameSiteValues[samesiteString]; ok { CookieSameSiteMode = samesite cfg.CookieSameSiteMode = CookieSameSiteMode } else { CookieSameSiteMode = http.SameSiteLaxMode cfg.CookieSameSiteMode = CookieSameSiteMode } } cfg.AllowEmbedding = security.Key("allow_embedding").MustBool(false) cfg.ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(true) cfg.XSSProtectionHeader = security.Key("x_xss_protection").MustBool(true) cfg.StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false) cfg.StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400) cfg.StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false) cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false) cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false) cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("") cfg.AngularSupportEnabled = security.Key("angular_support_enabled").MustBool(true) // read data source proxy whitelist DataProxyWhiteList = make(map[string]bool) securityStr := valueAsString(security, "data_source_proxy_whitelist", "") for _, hostAndIP := range util.SplitString(securityStr) { DataProxyWhiteList[hostAndIP] = true } // admin cfg.DisableInitAdminCreation = security.Key("disable_initial_admin_creation").MustBool(false) cfg.AdminUser = valueAsString(security, "admin_user", "") cfg.AdminPassword = valueAsString(security, "admin_password", "") return nil } func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { auth := iniFile.Section("auth") cfg.LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session") const defaultMaxInactiveLifetime = "7d" maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", defaultMaxInactiveLifetime) cfg.LoginMaxInactiveLifetime, err = gtime.ParseDuration(maxInactiveDurationVal) if err != nil { return err } const defaultMaxLifetime = "30d" maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", defaultMaxLifetime) cfg.LoginMaxLifetime, err = gtime.ParseDuration(maxLifetimeDurationVal) if err != nil { return err } cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1) cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10) if cfg.TokenRotationIntervalMinutes < 2 { cfg.TokenRotationIntervalMinutes = 2 } DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600) SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") cfg.OAuthSkipOrgRoleUpdateSync = auth.Key("oauth_skip_org_role_update_sync").MustBool(false) // SigV4 SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false) cfg.SigV4AuthEnabled = SigV4AuthEnabled cfg.SigV4VerboseLogging = auth.Key("sigv4_verbose_logging").MustBool(false) // Azure Auth AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false) cfg.AzureAuthEnabled = AzureAuthEnabled // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) cfg.AnonymousEnabled = AnonymousEnabled cfg.AnonymousOrgName = valueAsString(iniFile.Section("auth.anonymous"), "org_name", "") cfg.AnonymousOrgRole = valueAsString(iniFile.Section("auth.anonymous"), "org_role", "") cfg.AnonymousHideVersion = iniFile.Section("auth.anonymous").Key("hide_version").MustBool(false) // basic auth authBasic := iniFile.Section("auth.basic") BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) cfg.BasicAuthEnabled = BasicAuthEnabled // JWT auth authJWT := iniFile.Section("auth.jwt") cfg.JWTAuthEnabled = authJWT.Key("enabled").MustBool(false) cfg.JWTAuthHeaderName = valueAsString(authJWT, "header_name", "") cfg.JWTAuthURLLogin = authJWT.Key("url_login").MustBool(false) cfg.JWTAuthEmailClaim = valueAsString(authJWT, "email_claim", "") cfg.JWTAuthUsernameClaim = valueAsString(authJWT, "username_claim", "") cfg.JWTAuthExpectClaims = valueAsString(authJWT, "expect_claims", "{}") cfg.JWTAuthJWKSetURL = valueAsString(authJWT, "jwk_set_url", "") cfg.JWTAuthCacheTTL = authJWT.Key("cache_ttl").MustDuration(time.Minute * 60) cfg.JWTAuthKeyFile = valueAsString(authJWT, "key_file", "") cfg.JWTAuthJWKSetFile = valueAsString(authJWT, "jwk_set_file", "") cfg.JWTAuthAutoSignUp = authJWT.Key("auto_sign_up").MustBool(false) authProxy := iniFile.Section("auth.proxy") AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) cfg.AuthProxyEnabled = AuthProxyEnabled cfg.AuthProxyHeaderName = valueAsString(authProxy, "header_name", "") AuthProxyHeaderProperty = valueAsString(authProxy, "header_property", "") cfg.AuthProxyHeaderProperty = AuthProxyHeaderProperty cfg.AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) cfg.AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) cfg.AuthProxySyncTTL = authProxy.Key("sync_ttl").MustInt() cfg.AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "") cfg.AuthProxyHeaders = make(map[string]string) headers := valueAsString(authProxy, "headers", "") for _, propertyAndHeader := range util.SplitString(headers) { split := strings.SplitN(propertyAndHeader, ":", 2) if len(split) == 2 { cfg.AuthProxyHeaders[split[0]] = split[1] } } cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) return nil } func readAccessControlSettings(iniFile *ini.File, cfg *Cfg) { rbac := iniFile.Section("rbac") cfg.RBACEnabled = rbac.Key("enabled").MustBool(true) cfg.RBACPermissionCache = rbac.Key("permission_cache").MustBool(true) cfg.RBACBuiltInRoleAssignmentEnabled = rbac.Key("builtin_role_assignment_enabled").MustBool(false) } func readUserSettings(iniFile *ini.File, cfg *Cfg) error { users := iniFile.Section("users") AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) cfg.AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrg = cfg.AutoAssignOrg cfg.AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1) AutoAssignOrgId = cfg.AutoAssignOrgId cfg.AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) AutoAssignOrgRole = cfg.AutoAssignOrgRole VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) cfg.CaseInsensitiveLogin = users.Key("case_insensitive_login").MustBool(false) LoginHint = valueAsString(users, "login_hint", "") PasswordHint = valueAsString(users, "password_hint", "") cfg.DefaultTheme = valueAsString(users, "default_theme", "") cfg.DefaultLocale = valueAsString(users, "default_locale", "") cfg.HomePage = valueAsString(users, "home_page", "") ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "") ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "") ExternalUserMngInfo = valueAsString(users, "external_manage_info", "") ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false) cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false) userInviteMaxLifetimeVal := valueAsString(users, "user_invite_max_lifetime_duration", "24h") userInviteMaxLifetimeDuration, err := gtime.ParseDuration(userInviteMaxLifetimeVal) if err != nil { return err } cfg.UserInviteMaxLifetime = userInviteMaxLifetimeDuration if cfg.UserInviteMaxLifetime < time.Minute*15 { return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)") } cfg.HiddenUsers = make(map[string]struct{}) hiddenUsers := users.Key("hidden_users").MustString("") for _, user := range strings.Split(hiddenUsers, ",") { user = strings.TrimSpace(user) if user != "" { cfg.HiddenUsers[user] = struct{}{} } } return nil } func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { renderSec := iniFile.Section("rendering") cfg.RendererUrl = valueAsString(renderSec, "server_url", "") cfg.RendererCallbackUrl = valueAsString(renderSec, "callback_url", "") if cfg.RendererCallbackUrl == "" { cfg.RendererCallbackUrl = AppUrl } else { if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' { cfg.RendererCallbackUrl += "/" } _, err := url.Parse(cfg.RendererCallbackUrl) if err != nil { // XXX: Should return an error? cfg.Logger.Error("Invalid callback_url.", "url", cfg.RendererCallbackUrl, "error", err) os.Exit(1) } } cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30) cfg.ImagesDir = filepath.Join(cfg.DataPath, "png") cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv") return nil } func readAlertingSettings(iniFile *ini.File) error { alerting := iniFile.Section("alerting") enabled, err := alerting.Key("enabled").Bool() AlertingEnabled = nil if err == nil { AlertingEnabled = &enabled } ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) AlertingRenderLimit = alerting.Key("concurrent_render_limit").MustInt(5) AlertingErrorOrTimeout = valueAsString(alerting, "error_or_timeout", "alerting") AlertingNoDataOrNullValues = valueAsString(alerting, "nodata_or_nullvalues", "no_data") evaluationTimeoutSeconds := alerting.Key("evaluation_timeout_seconds").MustInt64(30) AlertingEvaluationTimeout = time.Second * time.Duration(evaluationTimeoutSeconds) notificationTimeoutSeconds := alerting.Key("notification_timeout_seconds").MustInt64(30) AlertingNotificationTimeout = time.Second * time.Duration(notificationTimeoutSeconds) AlertingMaxAttempts = alerting.Key("max_attempts").MustInt(3) AlertingMinInterval = alerting.Key("min_interval_seconds").MustInt64(1) return nil } // IsLegacyAlertingEnabled returns whether the legacy alerting is enabled or not. // It's safe to be used only after readAlertingSettings() and ReadUnifiedAlertingSettings() are executed. func IsLegacyAlertingEnabled() bool { return AlertingEnabled != nil && *AlertingEnabled } func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error { snapshots := iniFile.Section("snapshots") ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "") ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "") ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) cfg.SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false) return nil } func (cfg *Cfg) readServerSettings(iniFile *ini.File) error { server := iniFile.Section("server") var err error AppUrl, AppSubUrl, err = cfg.parseAppUrlAndSubUrl(server) if err != nil { return err } ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false) cfg.AppURL = AppUrl cfg.AppSubURL = AppSubUrl cfg.ServeFromSubPath = ServeFromSubPath cfg.Protocol = HTTPScheme protocolStr := valueAsString(server, "protocol", "http") if protocolStr == "https" { cfg.Protocol = HTTPSScheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() } if protocolStr == "h2" { cfg.Protocol = HTTP2Scheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() } if protocolStr == "socket" { cfg.Protocol = SocketScheme cfg.SocketPath = server.Key("socket").String() } cfg.Domain = valueAsString(server, "domain", "localhost") cfg.HTTPAddr = valueAsString(server, "http_addr", DefaultHTTPAddr) cfg.HTTPPort = valueAsString(server, "http_port", "3000") cfg.RouterLogging = server.Key("router_logging").MustBool(false) cfg.EnableGzip = server.Key("enable_gzip").MustBool(false) cfg.EnforceDomain = server.Key("enforce_domain").MustBool(false) staticRoot := valueAsString(server, "static_root_path", "") StaticRootPath = makeAbsolute(staticRoot, HomePath) cfg.StaticRootPath = StaticRootPath if err := cfg.validateStaticRootPath(); err != nil { return err } cdnURL := valueAsString(server, "cdn_url", "") if cdnURL != "" { cfg.CDNRootURL, err = url.Parse(cdnURL) if err != nil { return err } } cfg.ReadTimeout = server.Key("read_timeout").MustDuration(0) return nil } // GetContentDeliveryURL returns full content delivery URL with // added to URL func (cfg *Cfg) GetContentDeliveryURL(prefix string) string { if cfg.CDNRootURL != nil { url := *cfg.CDNRootURL preReleaseFolder := "" url.Path = path.Join(url.Path, prefix, preReleaseFolder, cfg.BuildVersion) return url.String() + "/" } return "" } func (cfg *Cfg) readDataSourcesSettings() { datasources := cfg.Raw.Section("datasources") cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000) } func GetAllowedOriginGlobs(originPatterns []string) ([]glob.Glob, error) { var originGlobs []glob.Glob allowedOrigins := originPatterns for _, originPattern := range allowedOrigins { g, err := glob.Compile(originPattern) if err != nil { return nil, fmt.Errorf("error parsing origin pattern: %v", err) } originGlobs = append(originGlobs, g) } return originGlobs, nil } func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { section := iniFile.Section("live") cfg.LiveMaxConnections = section.Key("max_connections").MustInt(100) if cfg.LiveMaxConnections < -1 { return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections) } cfg.LiveHAEngine = section.Key("ha_engine").MustString("") switch cfg.LiveHAEngine { case "", "redis": default: return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine) } cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379") var originPatterns []string allowedOrigins := section.Key("allowed_origins").MustString("") for _, originPattern := range strings.Split(allowedOrigins, ",") { originPattern = strings.TrimSpace(originPattern) if originPattern == "" { continue } originPatterns = append(originPatterns, originPattern) } _, err := GetAllowedOriginGlobs(originPatterns) if err != nil { return err } cfg.LiveAllowedOrigins = originPatterns return nil }