1// Copyright 2014 Unknwon
2// Copyright 2014 Torkel Ödegaard
3
4package setting
5
6import (
7	"bytes"
8	"encoding/json"
9	"errors"
10	"fmt"
11	"net/http"
12	"net/url"
13	"os"
14	"path"
15	"path/filepath"
16	"regexp"
17	"runtime"
18	"strconv"
19	"strings"
20	"time"
21
22	"github.com/grafana/grafana-aws-sdk/pkg/awsds"
23	"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
24
25	"github.com/grafana/grafana/pkg/infra/log"
26	"github.com/grafana/grafana/pkg/util"
27
28	"github.com/gobwas/glob"
29	"github.com/prometheus/common/model"
30	"gopkg.in/ini.v1"
31)
32
33type Scheme string
34
35const (
36	HTTPScheme   Scheme = "http"
37	HTTPSScheme  Scheme = "https"
38	HTTP2Scheme  Scheme = "h2"
39	SocketScheme Scheme = "socket"
40)
41
42const (
43	RedactedPassword = "*********"
44	DefaultHTTPAddr  = "0.0.0.0"
45	Dev              = "development"
46	Prod             = "production"
47	Test             = "test"
48	ApplicationName  = "Grafana"
49)
50
51// This constant corresponds to the default value for ldap_sync_ttl in .ini files
52// it is used for comparison and has to be kept in sync
53const (
54	authProxySyncTTL = 60
55)
56
57// zoneInfo names environment variable for setting the path to look for the timezone database in go
58const zoneInfo = "ZONEINFO"
59
60var (
61	// App settings.
62	Env              = Dev
63	AppUrl           string
64	AppSubUrl        string
65	ServeFromSubPath bool
66	InstanceName     string
67
68	// build
69	BuildVersion string
70	BuildCommit  string
71	BuildBranch  string
72	BuildStamp   int64
73	IsEnterprise bool
74
75	// packaging
76	Packaging = "unknown"
77
78	// Paths
79	HomePath       string
80	CustomInitPath = "conf/custom.ini"
81
82	// HTTP server options
83	StaticRootPath string
84
85	// Security settings.
86	SecretKey              string
87	DisableGravatar        bool
88	DataProxyWhiteList     map[string]bool
89	CookieSecure           bool
90	CookieSameSiteDisabled bool
91	CookieSameSiteMode     http.SameSite
92
93	// Snapshots
94	ExternalSnapshotUrl   string
95	ExternalSnapshotName  string
96	ExternalEnabled       bool
97	SnapShotRemoveExpired bool
98
99	// Dashboard history
100	DashboardVersionsToKeep int
101	MinRefreshInterval      string
102
103	// User settings
104	AllowUserSignUp         bool
105	AllowUserOrgCreate      bool
106	AutoAssignOrg           bool
107	AutoAssignOrgId         int
108	AutoAssignOrgRole       string
109	VerifyEmailEnabled      bool
110	LoginHint               string
111	PasswordHint            string
112	DisableLoginForm        bool
113	DisableSignoutMenu      bool
114	SignoutRedirectUrl      string
115	ExternalUserMngLinkUrl  string
116	ExternalUserMngLinkName string
117	ExternalUserMngInfo     string
118	OAuthAutoLogin          bool
119	ViewersCanEdit          bool
120
121	// HTTP auth
122	SigV4AuthEnabled bool
123
124	AnonymousEnabled bool
125
126	// Auth proxy settings
127	AuthProxyEnabled        bool
128	AuthProxyHeaderProperty string
129
130	// Basic Auth
131	BasicAuthEnabled bool
132
133	// Global setting objects.
134	Raw *ini.File
135
136	// for logging purposes
137	configFiles                  []string
138	appliedCommandLineProperties []string
139	appliedEnvOverrides          []string
140
141	// analytics
142	GoogleAnalyticsId       string
143	GoogleTagManagerId      string
144	RudderstackDataPlaneUrl string
145	RudderstackWriteKey     string
146
147	// LDAP
148	LDAPEnabled           bool
149	LDAPConfigFile        string
150	LDAPSyncCron          string
151	LDAPAllowSignup       bool
152	LDAPActiveSyncEnabled bool
153
154	// Quota
155	Quota QuotaSettings
156
157	// Alerting
158	AlertingEnabled            *bool
159	ExecuteAlerts              bool
160	AlertingRenderLimit        int
161	AlertingErrorOrTimeout     string
162	AlertingNoDataOrNullValues string
163
164	AlertingEvaluationTimeout   time.Duration
165	AlertingNotificationTimeout time.Duration
166	AlertingMaxAttempts         int
167	AlertingMinInterval         int64
168
169	// Explore UI
170	ExploreEnabled bool
171
172	// Grafana.NET URL
173	GrafanaComUrl string
174
175	ImageUploadProvider string
176)
177
178// AddChangePasswordLink returns if login form is disabled or not since
179// the same intention can be used to hide both features.
180func AddChangePasswordLink() bool {
181	return !DisableLoginForm
182}
183
184// TODO move all global vars to this struct
185type Cfg struct {
186	Raw    *ini.File
187	Logger log.Logger
188
189	// HTTP Server Settings
190	CertFile         string
191	KeyFile          string
192	HTTPAddr         string
193	HTTPPort         string
194	AppURL           string
195	AppSubURL        string
196	ServeFromSubPath bool
197	StaticRootPath   string
198	Protocol         Scheme
199	SocketPath       string
200	RouterLogging    bool
201	Domain           string
202	CDNRootURL       *url.URL
203	ReadTimeout      time.Duration
204	EnableGzip       bool
205	EnforceDomain    bool
206
207	// Security settings
208	SecretKey             string
209	EmailCodeValidMinutes int
210
211	// build
212	BuildVersion string
213	BuildCommit  string
214	BuildBranch  string
215	BuildStamp   int64
216	IsEnterprise bool
217
218	// packaging
219	Packaging string
220
221	// Paths
222	HomePath           string
223	ProvisioningPath   string
224	DataPath           string
225	LogsPath           string
226	PluginsPath        string
227	BundledPluginsPath string
228
229	// SMTP email settings
230	Smtp SmtpSettings
231
232	// Rendering
233	ImagesDir                      string
234	CSVsDir                        string
235	RendererUrl                    string
236	RendererCallbackUrl            string
237	RendererConcurrentRequestLimit int
238
239	// Security
240	DisableInitAdminCreation          bool
241	DisableBruteForceLoginProtection  bool
242	CookieSecure                      bool
243	CookieSameSiteDisabled            bool
244	CookieSameSiteMode                http.SameSite
245	AllowEmbedding                    bool
246	XSSProtectionHeader               bool
247	ContentTypeProtectionHeader       bool
248	StrictTransportSecurity           bool
249	StrictTransportSecurityMaxAge     int
250	StrictTransportSecurityPreload    bool
251	StrictTransportSecuritySubDomains bool
252	// CSPEnabled toggles Content Security Policy support.
253	CSPEnabled bool
254	// CSPTemplate contains the Content Security Policy template.
255	CSPTemplate string
256
257	TempDataLifetime                 time.Duration
258	PluginsEnableAlpha               bool
259	PluginsAppsSkipVerifyTLS         bool
260	PluginSettings                   PluginSettings
261	PluginsAllowUnsigned             []string
262	PluginCatalogURL                 string
263	PluginCatalogHiddenPlugins       []string
264	PluginAdminEnabled               bool
265	PluginAdminExternalManageEnabled bool
266	DisableSanitizeHtml              bool
267	EnterpriseLicensePath            string
268
269	// Metrics
270	MetricsEndpointEnabled           bool
271	MetricsEndpointBasicAuthUsername string
272	MetricsEndpointBasicAuthPassword string
273	MetricsEndpointDisableTotalStats bool
274	MetricsGrafanaEnvironmentInfo    map[string]string
275
276	// Dashboards
277	DefaultHomeDashboardPath string
278
279	// Auth
280	LoginCookieName              string
281	LoginMaxInactiveLifetime     time.Duration
282	LoginMaxLifetime             time.Duration
283	TokenRotationIntervalMinutes int
284	SigV4AuthEnabled             bool
285	BasicAuthEnabled             bool
286	AdminUser                    string
287	AdminPassword                string
288
289	// AWS Plugin Auth
290	AWSAllowedAuthProviders []string
291	AWSAssumeRoleEnabled    bool
292	AWSListMetricsPageLimit int
293
294	// Azure Cloud settings
295	Azure AzureSettings
296
297	// Auth proxy settings
298	AuthProxyEnabled          bool
299	AuthProxyHeaderName       string
300	AuthProxyHeaderProperty   string
301	AuthProxyAutoSignUp       bool
302	AuthProxyEnableLoginToken bool
303	AuthProxyWhitelist        string
304	AuthProxyHeaders          map[string]string
305	AuthProxySyncTTL          int
306
307	// OAuth
308	OAuthCookieMaxAge int
309
310	// JWT Auth
311	JWTAuthEnabled       bool
312	JWTAuthHeaderName    string
313	JWTAuthEmailClaim    string
314	JWTAuthUsernameClaim string
315	JWTAuthExpectClaims  string
316	JWTAuthJWKSetURL     string
317	JWTAuthCacheTTL      time.Duration
318	JWTAuthKeyFile       string
319	JWTAuthJWKSetFile    string
320
321	// Dataproxy
322	SendUserHeader                 bool
323	DataProxyLogging               bool
324	DataProxyTimeout               int
325	DataProxyDialTimeout           int
326	DataProxyTLSHandshakeTimeout   int
327	DataProxyExpectContinueTimeout int
328	DataProxyMaxConnsPerHost       int
329	DataProxyMaxIdleConns          int
330	DataProxyKeepAlive             int
331	DataProxyIdleConnTimeout       int
332	ResponseLimit                  int64
333	DataProxyRowLimit              int64
334
335	// DistributedCache
336	RemoteCacheOptions *RemoteCacheOptions
337
338	EditorsCanAdmin bool
339
340	ApiKeyMaxSecondsToLive int64
341
342	// Use to enable new features which may still be in alpha/beta stage.
343	FeatureToggles       map[string]bool
344	AnonymousEnabled     bool
345	AnonymousOrgName     string
346	AnonymousOrgRole     string
347	AnonymousHideVersion bool
348
349	DateFormats DateFormats
350
351	// User
352	UserInviteMaxLifetime time.Duration
353	HiddenUsers           map[string]struct{}
354
355	// Annotations
356	AnnotationCleanupJobBatchSize      int64
357	AlertingAnnotationCleanupSetting   AnnotationCleanupSettings
358	DashboardAnnotationCleanupSettings AnnotationCleanupSettings
359	APIAnnotationCleanupSettings       AnnotationCleanupSettings
360
361	// Sentry config
362	Sentry Sentry
363
364	// Data sources
365	DataSourceLimit int
366
367	// Snapshots
368	SnapshotPublicMode bool
369
370	ErrTemplateName string
371
372	Env string
373
374	// Analytics
375	CheckForUpdates                     bool
376	ReportingDistributor                string
377	ReportingEnabled                    bool
378	ApplicationInsightsConnectionString string
379	ApplicationInsightsEndpointUrl      string
380
381	// LDAP
382	LDAPEnabled     bool
383	LDAPAllowSignup bool
384
385	Quota QuotaSettings
386
387	DefaultTheme string
388	HomePage     string
389
390	AutoAssignOrg     bool
391	AutoAssignOrgId   int
392	AutoAssignOrgRole string
393
394	// ExpressionsEnabled specifies whether expressions are enabled.
395	ExpressionsEnabled bool
396
397	ImageUploadProvider string
398
399	// LiveMaxConnections is a maximum number of WebSocket connections to
400	// Grafana Live ws endpoint (per Grafana server instance). 0 disables
401	// Live, -1 means unlimited connections.
402	LiveMaxConnections int
403	// LiveHAEngine is a type of engine to use to achieve HA with Grafana Live.
404	// Zero value means in-memory single node setup.
405	LiveHAEngine string
406	// LiveHAEngineAddress is a connection address for Live HA engine.
407	LiveHAEngineAddress string
408	// LiveAllowedOrigins is a set of origins accepted by Live. If not provided
409	// then Live uses AppURL as the only allowed origin.
410	LiveAllowedOrigins []string
411
412	// Grafana.com URL
413	GrafanaComURL string
414
415	// Alerting
416
417	// AlertingBaseInterval controls the alerting base interval in seconds.
418	// Only for internal use and not user configuration.
419	AlertingBaseInterval time.Duration
420
421	// Geomap base layer config
422	GeomapDefaultBaseLayerConfig map[string]interface{}
423	GeomapEnableCustomBaseLayers bool
424
425	// Unified Alerting
426	UnifiedAlerting UnifiedAlertingSettings
427}
428
429// IsLiveConfigEnabled returns true if live should be able to save configs to SQL tables
430func (cfg Cfg) IsLiveConfigEnabled() bool {
431	return cfg.FeatureToggles["live-config"]
432}
433
434// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
435func (cfg Cfg) IsTrimDefaultsEnabled() bool {
436	return cfg.FeatureToggles["trimDefaults"]
437}
438
439// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
440func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
441	return cfg.FeatureToggles["database_metrics"]
442}
443
444// IsHTTPRequestHistogramDisabled returns whether the request historgrams is disabled.
445// This feature toggle will be removed in Grafana 8.x but gives the operator
446// some graceperiod to update all the monitoring tools.
447func (cfg Cfg) IsHTTPRequestHistogramDisabled() bool {
448	return cfg.FeatureToggles["disable_http_request_histogram"]
449}
450
451func (cfg Cfg) IsNewNavigationEnabled() bool {
452	return cfg.FeatureToggles["newNavigation"]
453}
454
455type CommandLineArgs struct {
456	Config   string
457	HomePath string
458	Args     []string
459}
460
461func (cfg Cfg) parseAppUrlAndSubUrl(section *ini.Section) (string, string, error) {
462	appUrl := valueAsString(section, "root_url", "http://localhost:3000/")
463
464	if appUrl[len(appUrl)-1] != '/' {
465		appUrl += "/"
466	}
467
468	// Check if has app suburl.
469	url, err := url.Parse(appUrl)
470	if err != nil {
471		cfg.Logger.Error("Invalid root_url.", "url", appUrl, "error", err)
472		os.Exit(1)
473	}
474
475	appSubUrl := strings.TrimSuffix(url.Path, "/")
476	return appUrl, appSubUrl, nil
477}
478
479func ToAbsUrl(relativeUrl string) string {
480	return AppUrl + relativeUrl
481}
482
483func RedactedValue(key, value string) string {
484	uppercased := strings.ToUpper(key)
485	// Sensitive information: password, secrets etc
486	for _, pattern := range []string{
487		"PASSWORD",
488		"SECRET",
489		"PROVIDER_CONFIG",
490		"PRIVATE_KEY",
491		"SECRET_KEY",
492		"CERTIFICATE",
493		"ACCOUNT_KEY",
494		"ENCRYPTION_KEY",
495		"VAULT_TOKEN",
496	} {
497		if match, err := regexp.MatchString(pattern, uppercased); match && err == nil {
498			return RedactedPassword
499		}
500	}
501
502	for _, exception := range []string{
503		"RUDDERSTACK",
504		"APPLICATION_INSIGHTS",
505		"SENTRY",
506	} {
507		if strings.Contains(uppercased, exception) {
508			return value
509		}
510	}
511
512	if u, err := RedactedURL(value); err == nil {
513		return u
514	}
515
516	return value
517}
518
519func RedactedURL(value string) (string, error) {
520	// Value could be a list of URLs
521	chunks := util.SplitString(value)
522
523	for i, chunk := range chunks {
524		var hasTmpPrefix bool
525		const tmpPrefix = "http://"
526
527		if !strings.Contains(chunk, "://") {
528			chunk = tmpPrefix + chunk
529			hasTmpPrefix = true
530		}
531
532		u, err := url.Parse(chunk)
533		if err != nil {
534			return "", err
535		}
536
537		redacted := u.Redacted()
538		if hasTmpPrefix {
539			redacted = strings.Replace(redacted, tmpPrefix, "", 1)
540		}
541
542		chunks[i] = redacted
543	}
544
545	if strings.Contains(value, ",") {
546		return strings.Join(chunks, ","), nil
547	}
548
549	return strings.Join(chunks, " "), nil
550}
551
552func applyEnvVariableOverrides(file *ini.File) error {
553	appliedEnvOverrides = make([]string, 0)
554	for _, section := range file.Sections() {
555		for _, key := range section.Keys() {
556			envKey := EnvKey(section.Name(), key.Name())
557			envValue := os.Getenv(envKey)
558
559			if len(envValue) > 0 {
560				key.SetValue(envValue)
561				appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue)))
562			}
563		}
564	}
565
566	return nil
567}
568
569func (cfg *Cfg) readGrafanaEnvironmentMetrics() error {
570	environmentMetricsSection := cfg.Raw.Section("metrics.environment_info")
571	keys := environmentMetricsSection.Keys()
572	cfg.MetricsGrafanaEnvironmentInfo = make(map[string]string, len(keys))
573
574	for _, key := range keys {
575		labelName := model.LabelName(key.Name())
576		labelValue := model.LabelValue(key.Value())
577
578		if !labelName.IsValid() {
579			return fmt.Errorf("invalid label name in [metrics.environment_info] configuration. name %q", labelName)
580		}
581
582		if !labelValue.IsValid() {
583			return fmt.Errorf("invalid label value in [metrics.environment_info] configuration. name %q value %q", labelName, labelValue)
584		}
585
586		cfg.MetricsGrafanaEnvironmentInfo[string(labelName)] = string(labelValue)
587	}
588
589	return nil
590}
591
592func (cfg *Cfg) readAnnotationSettings() {
593	section := cfg.Raw.Section("annotations")
594	cfg.AnnotationCleanupJobBatchSize = section.Key("cleanupjob_batchsize").MustInt64(100)
595
596	dashboardAnnotation := cfg.Raw.Section("annotations.dashboard")
597	apiIAnnotation := cfg.Raw.Section("annotations.api")
598	alertingSection := cfg.Raw.Section("alerting")
599
600	var newAnnotationCleanupSettings = func(section *ini.Section, maxAgeField string) AnnotationCleanupSettings {
601		maxAge, err := gtime.ParseDuration(section.Key(maxAgeField).MustString(""))
602		if err != nil {
603			maxAge = 0
604		}
605
606		return AnnotationCleanupSettings{
607			MaxAge:   maxAge,
608			MaxCount: section.Key("max_annotations_to_keep").MustInt64(0),
609		}
610	}
611
612	cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingSection, "max_annotation_age")
613	cfg.DashboardAnnotationCleanupSettings = newAnnotationCleanupSettings(dashboardAnnotation, "max_age")
614	cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age")
615}
616
617func (cfg *Cfg) readExpressionsSettings() {
618	expressions := cfg.Raw.Section("expressions")
619	cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true)
620}
621
622type AnnotationCleanupSettings struct {
623	MaxAge   time.Duration
624	MaxCount int64
625}
626
627func EnvKey(sectionName string, keyName string) string {
628	sN := strings.ToUpper(strings.ReplaceAll(sectionName, ".", "_"))
629	sN = strings.ReplaceAll(sN, "-", "_")
630	kN := strings.ToUpper(strings.ReplaceAll(keyName, ".", "_"))
631	envKey := fmt.Sprintf("GF_%s_%s", sN, kN)
632	return envKey
633}
634
635func applyCommandLineDefaultProperties(props map[string]string, file *ini.File) {
636	appliedCommandLineProperties = make([]string, 0)
637	for _, section := range file.Sections() {
638		for _, key := range section.Keys() {
639			keyString := fmt.Sprintf("default.%s.%s", section.Name(), key.Name())
640			value, exists := props[keyString]
641			if exists {
642				key.SetValue(value)
643				appliedCommandLineProperties = append(appliedCommandLineProperties,
644					fmt.Sprintf("%s=%s", keyString, RedactedValue(keyString, value)))
645			}
646		}
647	}
648}
649
650func applyCommandLineProperties(props map[string]string, file *ini.File) {
651	for _, section := range file.Sections() {
652		sectionName := section.Name() + "."
653		if section.Name() == ini.DefaultSection {
654			sectionName = ""
655		}
656		for _, key := range section.Keys() {
657			keyString := sectionName + key.Name()
658			value, exists := props[keyString]
659			if exists {
660				appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value))
661				key.SetValue(value)
662			}
663		}
664	}
665}
666
667func (cfg Cfg) getCommandLineProperties(args []string) map[string]string {
668	props := make(map[string]string)
669
670	for _, arg := range args {
671		if !strings.HasPrefix(arg, "cfg:") {
672			continue
673		}
674
675		trimmed := strings.TrimPrefix(arg, "cfg:")
676		parts := strings.Split(trimmed, "=")
677		if len(parts) != 2 {
678			cfg.Logger.Error("Invalid command line argument.", "argument", arg)
679			os.Exit(1)
680		}
681
682		props[parts[0]] = parts[1]
683	}
684	return props
685}
686
687func makeAbsolute(path string, root string) string {
688	if filepath.IsAbs(path) {
689		return path
690	}
691	return filepath.Join(root, path)
692}
693
694func (cfg *Cfg) loadSpecifiedConfigFile(configFile string, masterFile *ini.File) error {
695	if configFile == "" {
696		configFile = filepath.Join(cfg.HomePath, CustomInitPath)
697		// return without error if custom file does not exist
698		if !pathExists(configFile) {
699			return nil
700		}
701	}
702
703	userConfig, err := ini.Load(configFile)
704	if err != nil {
705		return fmt.Errorf("failed to parse %q: %w", configFile, err)
706	}
707
708	userConfig.BlockMode = false
709
710	for _, section := range userConfig.Sections() {
711		for _, key := range section.Keys() {
712			if key.Value() == "" {
713				continue
714			}
715
716			defaultSec, err := masterFile.GetSection(section.Name())
717			if err != nil {
718				defaultSec, _ = masterFile.NewSection(section.Name())
719			}
720			defaultKey, err := defaultSec.GetKey(key.Name())
721			if err != nil {
722				defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
723			}
724			defaultKey.SetValue(key.Value())
725		}
726	}
727
728	configFiles = append(configFiles, configFile)
729	return nil
730}
731
732func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
733	// load config defaults
734	defaultConfigFile := path.Join(HomePath, "conf/defaults.ini")
735	configFiles = append(configFiles, defaultConfigFile)
736
737	// check if config file exists
738	if _, err := os.Stat(defaultConfigFile); os.IsNotExist(err) {
739		fmt.Println("Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepath")
740		os.Exit(1)
741	}
742
743	// load defaults
744	parsedFile, err := ini.Load(defaultConfigFile)
745	if err != nil {
746		fmt.Printf("Failed to parse defaults.ini, %v\n", err)
747		os.Exit(1)
748		return nil, err
749	}
750
751	parsedFile.BlockMode = false
752
753	// command line props
754	commandLineProps := cfg.getCommandLineProperties(args.Args)
755	// load default overrides
756	applyCommandLineDefaultProperties(commandLineProps, parsedFile)
757
758	// load specified config file
759	err = cfg.loadSpecifiedConfigFile(args.Config, parsedFile)
760	if err != nil {
761		err2 := cfg.initLogging(parsedFile)
762		if err2 != nil {
763			return nil, err2
764		}
765		cfg.Logger.Error(err.Error())
766		os.Exit(1)
767	}
768
769	// apply environment overrides
770	err = applyEnvVariableOverrides(parsedFile)
771	if err != nil {
772		return nil, err
773	}
774
775	// apply command line overrides
776	applyCommandLineProperties(commandLineProps, parsedFile)
777
778	// evaluate config values containing environment variables
779	err = expandConfig(parsedFile)
780	if err != nil {
781		return nil, err
782	}
783
784	// update data path and logging config
785	dataPath := valueAsString(parsedFile.Section("paths"), "data", "")
786
787	cfg.DataPath = makeAbsolute(dataPath, HomePath)
788	err = cfg.initLogging(parsedFile)
789	if err != nil {
790		return nil, err
791	}
792
793	return parsedFile, err
794}
795
796func pathExists(path string) bool {
797	_, err := os.Stat(path)
798	if err == nil {
799		return true
800	}
801	if os.IsNotExist(err) {
802		return false
803	}
804	return false
805}
806
807func (cfg *Cfg) setHomePath(args CommandLineArgs) {
808	if args.HomePath != "" {
809		cfg.HomePath = args.HomePath
810		HomePath = cfg.HomePath
811		return
812	}
813
814	var err error
815	cfg.HomePath, err = filepath.Abs(".")
816	if err != nil {
817		panic(err)
818	}
819
820	HomePath = cfg.HomePath
821	// check if homepath is correct
822	if pathExists(filepath.Join(cfg.HomePath, "conf/defaults.ini")) {
823		return
824	}
825
826	// try down one path
827	if pathExists(filepath.Join(cfg.HomePath, "../conf/defaults.ini")) {
828		cfg.HomePath = filepath.Join(cfg.HomePath, "../")
829		HomePath = cfg.HomePath
830	}
831}
832
833var skipStaticRootValidation = false
834
835func NewCfg() *Cfg {
836	return &Cfg{
837		Logger: log.New("settings"),
838		Raw:    ini.Empty(),
839	}
840}
841
842func NewCfgFromArgs(args CommandLineArgs) (*Cfg, error) {
843	cfg := NewCfg()
844	if err := cfg.Load(args); err != nil {
845		return nil, err
846	}
847
848	return cfg, nil
849}
850
851func (cfg *Cfg) validateStaticRootPath() error {
852	if skipStaticRootValidation {
853		return nil
854	}
855
856	if _, err := os.Stat(path.Join(StaticRootPath, "build")); err != nil {
857		cfg.Logger.Error("Failed to detect generated javascript files in public/build")
858	}
859
860	return nil
861}
862
863func (cfg *Cfg) Load(args CommandLineArgs) error {
864	cfg.setHomePath(args)
865
866	// Fix for missing IANA db on Windows
867	_, zoneInfoSet := os.LookupEnv(zoneInfo)
868	if runtime.GOOS == "windows" && !zoneInfoSet {
869		if err := os.Setenv(zoneInfo, filepath.Join(HomePath, "tools", "zoneinfo.zip")); err != nil {
870			cfg.Logger.Error("Can't set ZONEINFO environment variable", "err", err)
871		}
872	}
873
874	iniFile, err := cfg.loadConfiguration(args)
875	if err != nil {
876		return err
877	}
878
879	cfg.Raw = iniFile
880
881	// Temporarily keep global, to make refactor in steps
882	Raw = cfg.Raw
883
884	cfg.BuildVersion = BuildVersion
885	cfg.BuildCommit = BuildCommit
886	cfg.BuildStamp = BuildStamp
887	cfg.BuildBranch = BuildBranch
888	cfg.IsEnterprise = IsEnterprise
889	cfg.Packaging = Packaging
890
891	cfg.ErrTemplateName = "error"
892
893	Env = valueAsString(iniFile.Section(""), "app_mode", "development")
894	cfg.Env = Env
895	InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name")
896	plugins := valueAsString(iniFile.Section("paths"), "plugins", "")
897	cfg.PluginsPath = makeAbsolute(plugins, HomePath)
898	cfg.BundledPluginsPath = makeAbsolute("plugins-bundled", HomePath)
899	provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
900	cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath)
901
902	if err := cfg.readServerSettings(iniFile); err != nil {
903		return err
904	}
905
906	if err := readDataProxySettings(iniFile, cfg); err != nil {
907		return err
908	}
909
910	if err := readSecuritySettings(iniFile, cfg); err != nil {
911		return err
912	}
913
914	if err := readSnapshotsSettings(cfg, iniFile); err != nil {
915		return err
916	}
917
918	// read dashboard settings
919	dashboards := iniFile.Section("dashboards")
920	DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
921	MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s")
922
923	cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("")
924
925	if err := readUserSettings(iniFile, cfg); err != nil {
926		return err
927	}
928	if err := readAuthSettings(iniFile, cfg); err != nil {
929		return err
930	}
931	if err := cfg.readRenderingSettings(iniFile); err != nil {
932		return err
933	}
934
935	cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
936	cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
937	cfg.MetricsEndpointBasicAuthUsername = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "")
938	cfg.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "")
939	cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false)
940
941	analytics := iniFile.Section("analytics")
942	cfg.CheckForUpdates = analytics.Key("check_for_updates").MustBool(true)
943	GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
944	GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
945	RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
946	RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String()
947	cfg.ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
948	cfg.ReportingDistributor = analytics.Key("reporting_distributor").MustString("grafana-labs")
949	if len(cfg.ReportingDistributor) >= 100 {
950		cfg.ReportingDistributor = cfg.ReportingDistributor[:100]
951	}
952	cfg.ApplicationInsightsConnectionString = analytics.Key("application_insights_connection_string").String()
953	cfg.ApplicationInsightsEndpointUrl = analytics.Key("application_insights_endpoint_url").String()
954
955	if err := readAlertingSettings(iniFile); err != nil {
956		return err
957	}
958
959	explore := iniFile.Section("explore")
960	ExploreEnabled = explore.Key("enabled").MustBool(true)
961
962	panelsSection := iniFile.Section("panels")
963	cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false)
964
965	if err := cfg.readPluginSettings(iniFile); err != nil {
966		return err
967	}
968
969	if err := cfg.readFeatureToggles(iniFile); err != nil {
970		return err
971	}
972
973	if err := cfg.ReadUnifiedAlertingSettings(iniFile); err != nil {
974		return err
975	}
976
977	// check old location for this option
978	if panelsSection.Key("enable_alpha").MustBool(false) {
979		cfg.PluginsEnableAlpha = true
980	}
981
982	cfg.readLDAPConfig()
983	cfg.handleAWSConfig()
984	cfg.readAzureSettings()
985	cfg.readSessionConfig()
986	cfg.readSmtpSettings()
987	cfg.readQuotaSettings()
988	cfg.readAnnotationSettings()
989	cfg.readExpressionsSettings()
990	if err := cfg.readGrafanaEnvironmentMetrics(); err != nil {
991		return err
992	}
993
994	cfg.readDataSourcesSettings()
995
996	if VerifyEmailEnabled && !cfg.Smtp.Enabled {
997		cfg.Logger.Warn("require_email_validation is enabled but smtp is disabled")
998	}
999
1000	// check old key  name
1001	GrafanaComUrl = valueAsString(iniFile.Section("grafana_net"), "url", "")
1002	if GrafanaComUrl == "" {
1003		GrafanaComUrl = valueAsString(iniFile.Section("grafana_com"), "url", "https://grafana.com")
1004	}
1005	cfg.GrafanaComURL = GrafanaComUrl
1006
1007	imageUploadingSection := iniFile.Section("external_image_storage")
1008	cfg.ImageUploadProvider = valueAsString(imageUploadingSection, "provider", "")
1009	ImageUploadProvider = cfg.ImageUploadProvider
1010
1011	enterprise := iniFile.Section("enterprise")
1012	cfg.EnterpriseLicensePath = valueAsString(enterprise, "license_path", filepath.Join(cfg.DataPath, "license.jwt"))
1013
1014	cacheServer := iniFile.Section("remote_cache")
1015	dbName := valueAsString(cacheServer, "type", "database")
1016	connStr := valueAsString(cacheServer, "connstr", "")
1017
1018	cfg.RemoteCacheOptions = &RemoteCacheOptions{
1019		Name:    dbName,
1020		ConnStr: connStr,
1021	}
1022
1023	geomapSection := iniFile.Section("geomap")
1024	basemapJSON := valueAsString(geomapSection, "default_baselayer_config", "")
1025	if basemapJSON != "" {
1026		layer := make(map[string]interface{})
1027		err = json.Unmarshal([]byte(basemapJSON), &layer)
1028		if err != nil {
1029			cfg.Logger.Error("Error reading json from default_baselayer_config", "error", err)
1030		} else {
1031			cfg.GeomapDefaultBaseLayerConfig = layer
1032		}
1033	}
1034	cfg.GeomapEnableCustomBaseLayers = geomapSection.Key("enable_custom_baselayers").MustBool(true)
1035
1036	cfg.readDateFormats()
1037	cfg.readSentryConfig()
1038
1039	if err := cfg.readLiveSettings(iniFile); err != nil {
1040		return err
1041	}
1042
1043	cfg.LogConfigSources()
1044
1045	return nil
1046}
1047
1048func valueAsString(section *ini.Section, keyName string, defaultValue string) string {
1049	return section.Key(keyName).MustString(defaultValue)
1050}
1051
1052type RemoteCacheOptions struct {
1053	Name    string
1054	ConnStr string
1055}
1056
1057func (cfg *Cfg) readLDAPConfig() {
1058	ldapSec := cfg.Raw.Section("auth.ldap")
1059	LDAPConfigFile = ldapSec.Key("config_file").String()
1060	LDAPSyncCron = ldapSec.Key("sync_cron").String()
1061	LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
1062	cfg.LDAPEnabled = LDAPEnabled
1063	LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
1064	LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
1065	cfg.LDAPAllowSignup = LDAPAllowSignup
1066}
1067
1068func (cfg *Cfg) handleAWSConfig() {
1069	awsPluginSec := cfg.Raw.Section("aws")
1070	cfg.AWSAssumeRoleEnabled = awsPluginSec.Key("assume_role_enabled").MustBool(true)
1071	allowedAuthProviders := awsPluginSec.Key("allowed_auth_providers").MustString("default,keys,credentials")
1072	for _, authProvider := range strings.Split(allowedAuthProviders, ",") {
1073		authProvider = strings.TrimSpace(authProvider)
1074		if authProvider != "" {
1075			cfg.AWSAllowedAuthProviders = append(cfg.AWSAllowedAuthProviders, authProvider)
1076		}
1077	}
1078	cfg.AWSListMetricsPageLimit = awsPluginSec.Key("list_metrics_page_limit").MustInt(500)
1079	// Also set environment variables that can be used by core plugins
1080	err := os.Setenv(awsds.AssumeRoleEnabledEnvVarKeyName, strconv.FormatBool(cfg.AWSAssumeRoleEnabled))
1081	if err != nil {
1082		cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AssumeRoleEnabledEnvVarKeyName), err)
1083	}
1084
1085	err = os.Setenv(awsds.AllowedAuthProvidersEnvVarKeyName, allowedAuthProviders)
1086	if err != nil {
1087		cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AllowedAuthProvidersEnvVarKeyName), err)
1088	}
1089}
1090
1091func (cfg *Cfg) readSessionConfig() {
1092	sec, _ := cfg.Raw.GetSection("session")
1093
1094	if sec != nil {
1095		cfg.Logger.Warn(
1096			"[Removed] Session setting was removed in v6.2, use remote_cache option instead",
1097		)
1098	}
1099}
1100
1101func (cfg *Cfg) initLogging(file *ini.File) error {
1102	logModeStr := valueAsString(file.Section("log"), "mode", "console")
1103	// split on comma
1104	logModes := strings.Split(logModeStr, ",")
1105	// also try space
1106	if len(logModes) == 1 {
1107		logModes = strings.Split(logModeStr, " ")
1108	}
1109	logsPath := valueAsString(file.Section("paths"), "logs", "")
1110	cfg.LogsPath = makeAbsolute(logsPath, HomePath)
1111	return log.ReadLoggingConfig(logModes, cfg.LogsPath, file)
1112}
1113
1114func (cfg *Cfg) LogConfigSources() {
1115	var text bytes.Buffer
1116
1117	for _, file := range configFiles {
1118		cfg.Logger.Info("Config loaded from", "file", file)
1119	}
1120
1121	if len(appliedCommandLineProperties) > 0 {
1122		for _, prop := range appliedCommandLineProperties {
1123			cfg.Logger.Info("Config overridden from command line", "arg", prop)
1124		}
1125	}
1126
1127	if len(appliedEnvOverrides) > 0 {
1128		text.WriteString("\tEnvironment variables used:\n")
1129		for _, prop := range appliedEnvOverrides {
1130			cfg.Logger.Info("Config overridden from Environment variable", "var", prop)
1131		}
1132	}
1133
1134	cfg.Logger.Info("Path Home", "path", HomePath)
1135	cfg.Logger.Info("Path Data", "path", cfg.DataPath)
1136	cfg.Logger.Info("Path Logs", "path", cfg.LogsPath)
1137	cfg.Logger.Info("Path Plugins", "path", cfg.PluginsPath)
1138	cfg.Logger.Info("Path Provisioning", "path", cfg.ProvisioningPath)
1139	cfg.Logger.Info("App mode " + cfg.Env)
1140}
1141
1142type DynamicSection struct {
1143	section *ini.Section
1144	Logger  log.Logger
1145}
1146
1147// Key dynamically overrides keys with environment variables.
1148// As a side effect, the value of the setting key will be updated if an environment variable is present.
1149func (s *DynamicSection) Key(k string) *ini.Key {
1150	envKey := EnvKey(s.section.Name(), k)
1151	envValue := os.Getenv(envKey)
1152	key := s.section.Key(k)
1153
1154	if len(envValue) == 0 {
1155		return key
1156	}
1157
1158	key.SetValue(envValue)
1159	s.Logger.Info("Config overridden from Environment variable", "var", fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue)))
1160
1161	return key
1162}
1163
1164// SectionWithEnvOverrides dynamically overrides keys with environment variables.
1165// As a side effect, the value of the setting key will be updated if an environment variable is present.
1166func (cfg *Cfg) SectionWithEnvOverrides(s string) *DynamicSection {
1167	return &DynamicSection{cfg.Raw.Section(s), cfg.Logger}
1168}
1169
1170func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
1171	security := iniFile.Section("security")
1172	SecretKey = valueAsString(security, "secret_key", "")
1173	cfg.SecretKey = SecretKey
1174	DisableGravatar = security.Key("disable_gravatar").MustBool(true)
1175	cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
1176
1177	CookieSecure = security.Key("cookie_secure").MustBool(false)
1178	cfg.CookieSecure = CookieSecure
1179
1180	samesiteString := valueAsString(security, "cookie_samesite", "lax")
1181
1182	if samesiteString == "disabled" {
1183		CookieSameSiteDisabled = true
1184		cfg.CookieSameSiteDisabled = CookieSameSiteDisabled
1185	} else {
1186		validSameSiteValues := map[string]http.SameSite{
1187			"lax":    http.SameSiteLaxMode,
1188			"strict": http.SameSiteStrictMode,
1189			"none":   http.SameSiteNoneMode,
1190		}
1191
1192		if samesite, ok := validSameSiteValues[samesiteString]; ok {
1193			CookieSameSiteMode = samesite
1194			cfg.CookieSameSiteMode = CookieSameSiteMode
1195		} else {
1196			CookieSameSiteMode = http.SameSiteLaxMode
1197			cfg.CookieSameSiteMode = CookieSameSiteMode
1198		}
1199	}
1200	cfg.AllowEmbedding = security.Key("allow_embedding").MustBool(false)
1201
1202	cfg.ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(true)
1203	cfg.XSSProtectionHeader = security.Key("x_xss_protection").MustBool(true)
1204	cfg.StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false)
1205	cfg.StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
1206	cfg.StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
1207	cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
1208	cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false)
1209	cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("")
1210
1211	// read data source proxy whitelist
1212	DataProxyWhiteList = make(map[string]bool)
1213	securityStr := valueAsString(security, "data_source_proxy_whitelist", "")
1214
1215	for _, hostAndIP := range util.SplitString(securityStr) {
1216		DataProxyWhiteList[hostAndIP] = true
1217	}
1218
1219	// admin
1220	cfg.DisableInitAdminCreation = security.Key("disable_initial_admin_creation").MustBool(false)
1221	cfg.AdminUser = valueAsString(security, "admin_user", "")
1222	cfg.AdminPassword = valueAsString(security, "admin_password", "")
1223
1224	return nil
1225}
1226
1227func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
1228	auth := iniFile.Section("auth")
1229
1230	cfg.LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session")
1231	maxInactiveDaysVal := auth.Key("login_maximum_inactive_lifetime_days").MustString("")
1232	if maxInactiveDaysVal != "" {
1233		maxInactiveDaysVal = fmt.Sprintf("%sd", maxInactiveDaysVal)
1234		cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_inactive_lifetime_days' is deprecated, please use 'login_maximum_inactive_lifetime_duration' instead")
1235	} else {
1236		maxInactiveDaysVal = "7d"
1237	}
1238	maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", maxInactiveDaysVal)
1239	cfg.LoginMaxInactiveLifetime, err = gtime.ParseDuration(maxInactiveDurationVal)
1240	if err != nil {
1241		return err
1242	}
1243
1244	maxLifetimeDaysVal := auth.Key("login_maximum_lifetime_days").MustString("")
1245	if maxLifetimeDaysVal != "" {
1246		maxLifetimeDaysVal = fmt.Sprintf("%sd", maxLifetimeDaysVal)
1247		cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_lifetime_days' is deprecated, please use 'login_maximum_lifetime_duration' instead")
1248	} else {
1249		maxLifetimeDaysVal = "30d"
1250	}
1251	maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", maxLifetimeDaysVal)
1252	cfg.LoginMaxLifetime, err = gtime.ParseDuration(maxLifetimeDurationVal)
1253	if err != nil {
1254		return err
1255	}
1256
1257	cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1)
1258
1259	cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)
1260	if cfg.TokenRotationIntervalMinutes < 2 {
1261		cfg.TokenRotationIntervalMinutes = 2
1262	}
1263
1264	DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
1265	DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
1266	OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
1267	cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600)
1268	SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "")
1269
1270	// SigV4
1271	SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false)
1272	cfg.SigV4AuthEnabled = SigV4AuthEnabled
1273
1274	// anonymous access
1275	AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false)
1276	cfg.AnonymousEnabled = AnonymousEnabled
1277	cfg.AnonymousOrgName = valueAsString(iniFile.Section("auth.anonymous"), "org_name", "")
1278	cfg.AnonymousOrgRole = valueAsString(iniFile.Section("auth.anonymous"), "org_role", "")
1279	cfg.AnonymousHideVersion = iniFile.Section("auth.anonymous").Key("hide_version").MustBool(false)
1280
1281	// basic auth
1282	authBasic := iniFile.Section("auth.basic")
1283	BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
1284	cfg.BasicAuthEnabled = BasicAuthEnabled
1285
1286	// JWT auth
1287	authJWT := iniFile.Section("auth.jwt")
1288	cfg.JWTAuthEnabled = authJWT.Key("enabled").MustBool(false)
1289	cfg.JWTAuthHeaderName = valueAsString(authJWT, "header_name", "")
1290	cfg.JWTAuthEmailClaim = valueAsString(authJWT, "email_claim", "")
1291	cfg.JWTAuthUsernameClaim = valueAsString(authJWT, "username_claim", "")
1292	cfg.JWTAuthExpectClaims = valueAsString(authJWT, "expect_claims", "{}")
1293	cfg.JWTAuthJWKSetURL = valueAsString(authJWT, "jwk_set_url", "")
1294	cfg.JWTAuthCacheTTL = authJWT.Key("cache_ttl").MustDuration(time.Minute * 60)
1295	cfg.JWTAuthKeyFile = valueAsString(authJWT, "key_file", "")
1296	cfg.JWTAuthJWKSetFile = valueAsString(authJWT, "jwk_set_file", "")
1297
1298	authProxy := iniFile.Section("auth.proxy")
1299	AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)
1300	cfg.AuthProxyEnabled = AuthProxyEnabled
1301
1302	cfg.AuthProxyHeaderName = valueAsString(authProxy, "header_name", "")
1303	AuthProxyHeaderProperty = valueAsString(authProxy, "header_property", "")
1304	cfg.AuthProxyHeaderProperty = AuthProxyHeaderProperty
1305	cfg.AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
1306	cfg.AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false)
1307
1308	ldapSyncVal := authProxy.Key("ldap_sync_ttl").MustInt()
1309	syncVal := authProxy.Key("sync_ttl").MustInt()
1310
1311	if ldapSyncVal != authProxySyncTTL {
1312		cfg.AuthProxySyncTTL = ldapSyncVal
1313		cfg.Logger.Warn("[Deprecated] the configuration setting 'ldap_sync_ttl' is deprecated, please use 'sync_ttl' instead")
1314	} else {
1315		cfg.AuthProxySyncTTL = syncVal
1316	}
1317
1318	cfg.AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "")
1319
1320	cfg.AuthProxyHeaders = make(map[string]string)
1321	headers := valueAsString(authProxy, "headers", "")
1322
1323	for _, propertyAndHeader := range util.SplitString(headers) {
1324		split := strings.SplitN(propertyAndHeader, ":", 2)
1325		if len(split) == 2 {
1326			cfg.AuthProxyHeaders[split[0]] = split[1]
1327		}
1328	}
1329
1330	return nil
1331}
1332
1333func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
1334	users := iniFile.Section("users")
1335	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
1336	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
1337	cfg.AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
1338	AutoAssignOrg = cfg.AutoAssignOrg
1339	cfg.AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
1340	AutoAssignOrgId = cfg.AutoAssignOrgId
1341	cfg.AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
1342	AutoAssignOrgRole = cfg.AutoAssignOrgRole
1343	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
1344
1345	LoginHint = valueAsString(users, "login_hint", "")
1346	PasswordHint = valueAsString(users, "password_hint", "")
1347	cfg.DefaultTheme = valueAsString(users, "default_theme", "")
1348	cfg.HomePage = valueAsString(users, "home_page", "")
1349	ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
1350	ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")
1351	ExternalUserMngInfo = valueAsString(users, "external_manage_info", "")
1352
1353	ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
1354	cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false)
1355
1356	userInviteMaxLifetimeVal := valueAsString(users, "user_invite_max_lifetime_duration", "24h")
1357	userInviteMaxLifetimeDuration, err := gtime.ParseDuration(userInviteMaxLifetimeVal)
1358	if err != nil {
1359		return err
1360	}
1361
1362	cfg.UserInviteMaxLifetime = userInviteMaxLifetimeDuration
1363	if cfg.UserInviteMaxLifetime < time.Minute*15 {
1364		return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)")
1365	}
1366
1367	cfg.HiddenUsers = make(map[string]struct{})
1368	hiddenUsers := users.Key("hidden_users").MustString("")
1369	for _, user := range strings.Split(hiddenUsers, ",") {
1370		user = strings.TrimSpace(user)
1371		if user != "" {
1372			cfg.HiddenUsers[user] = struct{}{}
1373		}
1374	}
1375
1376	return nil
1377}
1378
1379func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error {
1380	renderSec := iniFile.Section("rendering")
1381	cfg.RendererUrl = valueAsString(renderSec, "server_url", "")
1382	cfg.RendererCallbackUrl = valueAsString(renderSec, "callback_url", "")
1383
1384	if cfg.RendererCallbackUrl == "" {
1385		cfg.RendererCallbackUrl = AppUrl
1386	} else {
1387		if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' {
1388			cfg.RendererCallbackUrl += "/"
1389		}
1390		_, err := url.Parse(cfg.RendererCallbackUrl)
1391		if err != nil {
1392			// XXX: Should return an error?
1393			cfg.Logger.Error("Invalid callback_url.", "url", cfg.RendererCallbackUrl, "error", err)
1394			os.Exit(1)
1395		}
1396	}
1397
1398	cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30)
1399	cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
1400	cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv")
1401
1402	return nil
1403}
1404
1405func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
1406	// Read and populate feature toggles list
1407	featureTogglesSection := iniFile.Section("feature_toggles")
1408	cfg.FeatureToggles = make(map[string]bool)
1409	featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
1410	for _, feature := range util.SplitString(featuresTogglesStr) {
1411		cfg.FeatureToggles[feature] = true
1412	}
1413	return nil
1414}
1415
1416func readAlertingSettings(iniFile *ini.File) error {
1417	alerting := iniFile.Section("alerting")
1418	enabled, err := alerting.Key("enabled").Bool()
1419	AlertingEnabled = nil
1420	if err == nil {
1421		AlertingEnabled = &enabled
1422	}
1423	ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true)
1424	AlertingRenderLimit = alerting.Key("concurrent_render_limit").MustInt(5)
1425
1426	AlertingErrorOrTimeout = valueAsString(alerting, "error_or_timeout", "alerting")
1427	AlertingNoDataOrNullValues = valueAsString(alerting, "nodata_or_nullvalues", "no_data")
1428
1429	evaluationTimeoutSeconds := alerting.Key("evaluation_timeout_seconds").MustInt64(30)
1430	AlertingEvaluationTimeout = time.Second * time.Duration(evaluationTimeoutSeconds)
1431	notificationTimeoutSeconds := alerting.Key("notification_timeout_seconds").MustInt64(30)
1432	AlertingNotificationTimeout = time.Second * time.Duration(notificationTimeoutSeconds)
1433	AlertingMaxAttempts = alerting.Key("max_attempts").MustInt(3)
1434	AlertingMinInterval = alerting.Key("min_interval_seconds").MustInt64(1)
1435
1436	return nil
1437}
1438
1439func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error {
1440	snapshots := iniFile.Section("snapshots")
1441
1442	ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "")
1443	ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "")
1444
1445	ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
1446	SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true)
1447	cfg.SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false)
1448
1449	return nil
1450}
1451
1452func (cfg *Cfg) readServerSettings(iniFile *ini.File) error {
1453	server := iniFile.Section("server")
1454	var err error
1455	AppUrl, AppSubUrl, err = cfg.parseAppUrlAndSubUrl(server)
1456	if err != nil {
1457		return err
1458	}
1459	ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false)
1460
1461	cfg.AppURL = AppUrl
1462	cfg.AppSubURL = AppSubUrl
1463	cfg.ServeFromSubPath = ServeFromSubPath
1464	cfg.Protocol = HTTPScheme
1465
1466	protocolStr := valueAsString(server, "protocol", "http")
1467
1468	if protocolStr == "https" {
1469		cfg.Protocol = HTTPSScheme
1470		cfg.CertFile = server.Key("cert_file").String()
1471		cfg.KeyFile = server.Key("cert_key").String()
1472	}
1473	if protocolStr == "h2" {
1474		cfg.Protocol = HTTP2Scheme
1475		cfg.CertFile = server.Key("cert_file").String()
1476		cfg.KeyFile = server.Key("cert_key").String()
1477	}
1478	if protocolStr == "socket" {
1479		cfg.Protocol = SocketScheme
1480		cfg.SocketPath = server.Key("socket").String()
1481	}
1482
1483	cfg.Domain = valueAsString(server, "domain", "localhost")
1484	cfg.HTTPAddr = valueAsString(server, "http_addr", DefaultHTTPAddr)
1485	cfg.HTTPPort = valueAsString(server, "http_port", "3000")
1486	cfg.RouterLogging = server.Key("router_logging").MustBool(false)
1487
1488	cfg.EnableGzip = server.Key("enable_gzip").MustBool(false)
1489	cfg.EnforceDomain = server.Key("enforce_domain").MustBool(false)
1490	staticRoot := valueAsString(server, "static_root_path", "")
1491	StaticRootPath = makeAbsolute(staticRoot, HomePath)
1492	cfg.StaticRootPath = StaticRootPath
1493
1494	if err := cfg.validateStaticRootPath(); err != nil {
1495		return err
1496	}
1497
1498	cdnURL := valueAsString(server, "cdn_url", "")
1499	if cdnURL != "" {
1500		cfg.CDNRootURL, err = url.Parse(cdnURL)
1501		if err != nil {
1502			return err
1503		}
1504	}
1505
1506	cfg.ReadTimeout = server.Key("read_timeout").MustDuration(0)
1507
1508	return nil
1509}
1510
1511// GetContentDeliveryURL returns full content delivery URL with /<edition>/<version> added to URL
1512func (cfg *Cfg) GetContentDeliveryURL(prefix string) string {
1513	if cfg.CDNRootURL != nil {
1514		url := *cfg.CDNRootURL
1515		preReleaseFolder := ""
1516
1517		url.Path = path.Join(url.Path, prefix, preReleaseFolder, cfg.BuildVersion)
1518		return url.String() + "/"
1519	}
1520
1521	return ""
1522}
1523
1524func (cfg *Cfg) readDataSourcesSettings() {
1525	datasources := cfg.Raw.Section("datasources")
1526	cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
1527}
1528
1529func GetAllowedOriginGlobs(originPatterns []string) ([]glob.Glob, error) {
1530	var originGlobs []glob.Glob
1531	allowedOrigins := originPatterns
1532	for _, originPattern := range allowedOrigins {
1533		g, err := glob.Compile(originPattern)
1534		if err != nil {
1535			return nil, fmt.Errorf("error parsing origin pattern: %v", err)
1536		}
1537		originGlobs = append(originGlobs, g)
1538	}
1539	return originGlobs, nil
1540}
1541
1542func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
1543	section := iniFile.Section("live")
1544	cfg.LiveMaxConnections = section.Key("max_connections").MustInt(100)
1545	if cfg.LiveMaxConnections < -1 {
1546		return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections)
1547	}
1548	cfg.LiveHAEngine = section.Key("ha_engine").MustString("")
1549	switch cfg.LiveHAEngine {
1550	case "", "redis":
1551	default:
1552		return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine)
1553	}
1554	cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379")
1555
1556	var originPatterns []string
1557	allowedOrigins := section.Key("allowed_origins").MustString("")
1558	for _, originPattern := range strings.Split(allowedOrigins, ",") {
1559		originPattern = strings.TrimSpace(originPattern)
1560		if originPattern == "" {
1561			continue
1562		}
1563		originPatterns = append(originPatterns, originPattern)
1564	}
1565	_, err := GetAllowedOriginGlobs(originPatterns)
1566	if err != nil {
1567		return err
1568	}
1569	cfg.LiveAllowedOrigins = originPatterns
1570	return nil
1571}
1572