grafana/pkg/setting/setting.go

2017 lines
65 KiB
Go

// Copyright 2014 Unknwon
// Copyright 2014 Torkel Ödegaard
package setting
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/gobwas/glob"
"github.com/prometheus/common/model"
"gopkg.in/ini.v1"
"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/models/roletype"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/osutil"
)
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"
ApplicationName = "Grafana"
)
// zoneInfo names environment variable for setting the path to look for the timezone database in go
const zoneInfo = "ZONEINFO"
var (
customInitPath = "conf/custom.ini"
// App settings.
Env = Dev
AppUrl string
AppSubUrl string
// build
BuildVersion string
BuildCommit string
EnterpriseBuildCommit string
BuildBranch string
BuildStamp int64
IsEnterprise bool
// packaging
Packaging = "unknown"
CookieSecure bool
CookieSameSiteDisabled bool
CookieSameSiteMode http.SameSite
)
// TODO move all global vars to this struct
type Cfg struct {
Target []string
Raw *ini.File
Logger log.Logger
// for logging purposes
configFiles []string
appliedCommandLineProperties []string
appliedEnvOverrides []string
// HTTP Server Settings
CertFile string
KeyFile string
HTTPAddr string
HTTPPort string
Env string
AppURL string
AppSubURL string
InstanceName string
ServeFromSubPath bool
StaticRootPath string
Protocol Scheme
SocketGid int
SocketMode int
SocketPath string
RouterLogging bool
Domain string
CDNRootURL *url.URL
ReadTimeout time.Duration
EnableGzip bool
EnforceDomain bool
MinTLSVersion string
// Security settings
SecretKey string
EmailCodeValidMinutes int
// build
BuildVersion string
BuildCommit string
EnterpriseBuildCommit string
BuildBranch string
BuildStamp int64
IsEnterprise bool
// packaging
Packaging string
// Paths
HomePath string
ProvisioningPath string
DataPath string
LogsPath string
PluginsPath string
BundledPluginsPath string
EnterpriseLicensePath string
// SMTP email settings
Smtp SmtpSettings
// Rendering
ImagesDir string
CSVsDir string
PDFsDir string
RendererUrl string
RendererCallbackUrl string
RendererAuthToken string
RendererConcurrentRequestLimit int
RendererRenderKeyLifeTime time.Duration
RendererDefaultImageWidth int
RendererDefaultImageHeight int
RendererDefaultImageScale float64
// 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
// CSPReportEnabled toggles Content Security Policy Report Only support.
CSPReportOnlyEnabled bool
// CSPReportOnlyTemplate contains the Content Security Policy Report Only template.
CSPReportOnlyTemplate string
AngularSupportEnabled bool
DisableFrontendSandboxForPlugins []string
DisableGravatar bool
DataProxyWhiteList map[string]bool
TempDataLifetime time.Duration
// Plugins
PluginsEnableAlpha bool
PluginsAppsSkipVerifyTLS bool
PluginSettings PluginSettings
PluginsAllowUnsigned []string
PluginCatalogURL string
PluginCatalogHiddenPlugins []string
PluginAdminEnabled bool
PluginAdminExternalManageEnabled bool
PluginForcePublicKeyDownload bool
PluginSkipPublicKeyDownload bool
DisablePlugins []string
HideAngularDeprecation []string
PluginInstallToken string
ForwardHostEnvVars []string
PluginsCDNURLTemplate string
PluginLogBackendRequests bool
// Panels
DisableSanitizeHtml bool
// Metrics
MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
MetricsEndpointDisableTotalStats bool
// MetricsIncludeTeamLabel configures grafana to set a label for
// the team responsible for the code at Grafana labs. We don't expect anyone else to
// use this setting.
MetricsIncludeTeamLabel bool
MetricsTotalStatsIntervalSeconds int
MetricsGrafanaEnvironmentInfo map[string]string
// Dashboards
DashboardVersionsToKeep int
MinRefreshInterval string
DefaultHomeDashboardPath string
// Auth
LoginCookieName string
LoginMaxInactiveLifetime time.Duration
LoginMaxLifetime time.Duration
TokenRotationIntervalMinutes int
SigV4AuthEnabled bool
SigV4VerboseLogging bool
AzureAuthEnabled bool
AzureSkipOrgRoleSync bool
BasicAuthEnabled bool
BasicAuthStrongPasswordPolicy bool
AdminUser string
AdminPassword string
DisableLogin bool
AdminEmail string
DisableLoginForm bool
SignoutRedirectUrl string
IDResponseHeaderEnabled bool
IDResponseHeaderPrefix string
IDResponseHeaderNamespaces map[string]struct{}
// Not documented & not supported
// stand in until a more complete solution is implemented
AuthConfigUIAdminAccess bool
// AWS Plugin Auth
AWSAllowedAuthProviders []string
AWSAssumeRoleEnabled bool
AWSSessionDuration string
AWSExternalId string
AWSListMetricsPageLimit int
AWSForwardSettingsPlugins []string
// Azure Cloud settings
Azure *azsettings.AzureSettings
// Auth proxy settings
AuthProxy AuthProxySettings
// OAuth
OAuthAutoLogin bool
OAuthCookieMaxAge int
OAuthAllowInsecureEmailLookup bool
JWTAuth AuthJWTSettings
// Extended JWT Auth
ExtendedJWTAuthEnabled bool
ExtendedJWTExpectIssuer string
ExtendedJWTExpectAudience string
// SSO Settings Auth
SSOSettingsReloadInterval time.Duration
SSOSettingsConfigurableProviders map[string]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
DataProxyUserAgent string
// DistributedCache
RemoteCacheOptions *RemoteCacheOptions
ViewersCanEdit bool
EditorsCanAdmin bool
ApiKeyMaxSecondsToLive int64
// Check if a feature toggle is enabled
// Deprecated: use featuremgmt.FeatureFlags
IsFeatureToggleEnabled func(key string) bool // filled in dynamically
AnonymousEnabled bool
AnonymousOrgName string
AnonymousOrgRole string
AnonymousHideVersion bool
AnonymousDeviceLimit int64
DateFormats DateFormats
// User
UserInviteMaxLifetime time.Duration
HiddenUsers map[string]struct{}
CaseInsensitiveLogin bool // Login and Email will be considered case insensitive
VerificationEmailMaxLifetime time.Duration
// Service Accounts
SATokenExpirationDayLimit int
// Annotations
AnnotationCleanupJobBatchSize int64
AnnotationMaximumTagsLength int64
AlertingAnnotationCleanupSetting AnnotationCleanupSettings
DashboardAnnotationCleanupSettings AnnotationCleanupSettings
APIAnnotationCleanupSettings AnnotationCleanupSettings
// GrafanaJavascriptAgent config
GrafanaJavascriptAgent GrafanaJavascriptAgent
// Data sources
DataSourceLimit int
// Number of queries to be executed concurrently. Only for the datasource supports concurrency.
ConcurrentQueryCount int
// IP range access control
IPRangeACEnabled bool
IPRangeACAllowedURLs []*url.URL
IPRangeACSecretKey string
// SQL Data sources
SqlDatasourceMaxOpenConnsDefault int
SqlDatasourceMaxIdleConnsDefault int
SqlDatasourceMaxConnLifetimeDefault int
// Snapshots
SnapshotEnabled bool
ExternalSnapshotUrl string
ExternalSnapshotName string
ExternalEnabled bool
// Deprecated: setting this to false adds deprecation warnings at runtime
SnapShotRemoveExpired bool
// Only used in https://snapshots.raintank.io/
SnapshotPublicMode bool
ErrTemplateName string
StackID string
Slug string
LocalFileSystemAvailable bool
// Deprecated
ForceMigration bool
// Analytics
CheckForGrafanaUpdates bool
CheckForPluginUpdates bool
ReportingDistributor string
ReportingEnabled bool
ApplicationInsightsConnectionString string
ApplicationInsightsEndpointUrl string
FeedbackLinksEnabled bool
// Frontend analytics
GoogleAnalyticsID string
GoogleAnalytics4ID string
GoogleAnalytics4SendManualPageViews bool
GoogleTagManagerID string
RudderstackDataPlaneURL string
RudderstackWriteKey string
RudderstackSDKURL string
RudderstackConfigURL string
RudderstackIntegrationsURL string
IntercomSecret string
// LDAP
LDAPAuthEnabled bool
LDAPSkipOrgRoleSync bool
LDAPConfigFilePath string
LDAPAllowSignup bool
LDAPActiveSyncEnabled bool
LDAPSyncCron string
DefaultTheme string
DefaultLanguage string
HomePage string
Quota QuotaSettings
// User settings
AllowUserSignUp bool
AllowUserOrgCreate bool
VerifyEmailEnabled bool
LoginHint string
PasswordHint string
DisableSignoutMenu bool
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo 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
LiveHAEnginePassword 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, used for OAuth redirect.
GrafanaComURL string
// Grafana.com API URL. Can be set separately to GrafanaComURL
// in case API is not publicly accessible.
// Defaults to GrafanaComURL setting + "/api" if unset.
GrafanaComAPIURL string
// Geomap base layer config
GeomapDefaultBaseLayerConfig map[string]any
GeomapEnableCustomBaseLayers bool
// Unified Alerting
UnifiedAlerting UnifiedAlertingSettings
// Query history
QueryHistoryEnabled bool
Storage StorageSettings
Search SearchSettings
SecureSocksDSProxy SecureSocksDSProxySettings
// SAML Auth
SAMLAuthEnabled bool
SAMLSkipOrgRoleSync bool
SAMLRoleValuesGrafanaAdmin string
// OAuth2 Server
OAuth2ServerEnabled bool
// OAuth2Server supports the two recommended key types from the RFC https://www.rfc-editor.org/rfc/rfc7518#section-3.1: RS256 and ES256
OAuth2ServerGeneratedKeyTypeForClient string
OAuth2ServerAccessTokenLifespan time.Duration
// Access Control
RBACPermissionCache bool
// Enable Permission validation during role creation and provisioning
RBACPermissionValidationEnabled bool
// Reset basic roles permissions on start-up
RBACResetBasicRoles bool
// RBAC single organization. This configuration option is subject to change.
RBACSingleOrganization bool
// GRPC Server.
GRPCServerNetwork string
GRPCServerAddress string
GRPCServerTLSConfig *tls.Config
CustomResponseHeaders map[string]string
// This is used to override the general error message shown to users when we want to obfuscate a sensitive backend error
UserFacingDefaultError string
// DatabaseInstrumentQueries is used to decide if database queries
// should be instrumented with metrics, logs and traces.
// This needs to be on the global object since its used in the
// sqlstore package and HTTP middlewares.
DatabaseInstrumentQueries bool
// Public dashboards
PublicDashboardsEnabled bool
// Cloud Migration
CloudMigrationIsTarget bool
// Feature Management Settings
FeatureManagement FeatureMgmtSettings
// 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
// News Feed
NewsFeedEnabled bool
// Experimental scope settings
ScopesListScopesURL string
ScopesListDashboardsURL string
}
// AddChangePasswordLink returns if login form is disabled or not since
// the same intention can be used to hide both features.
func (cfg *Cfg) AddChangePasswordLink() bool {
return !(cfg.DisableLoginForm || cfg.DisableLogin)
}
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 {
if value == "" {
return ""
}
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",
"CLIENT_SECRET",
"ENTERPRISE_LICENSE",
} {
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 (cfg *Cfg) applyEnvVariableOverrides(file *ini.File) error {
cfg.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)
cfg.appliedEnvOverrides = append(cfg.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))
cfg.MetricsGrafanaEnvironmentInfo["version"] = cfg.BuildVersion
cfg.MetricsGrafanaEnvironmentInfo["commit"] = cfg.BuildCommit
if cfg.EnterpriseBuildCommit != "NA" && cfg.EnterpriseBuildCommit != "" {
cfg.MetricsGrafanaEnvironmentInfo["enterprise_commit"] = cfg.EnterpriseBuildCommit
}
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() error {
section := cfg.Raw.Section("annotations")
cfg.AnnotationCleanupJobBatchSize = section.Key("cleanupjob_batchsize").MustInt64(100)
cfg.AnnotationMaximumTagsLength = section.Key("tags_length").MustInt64(500)
switch {
case cfg.AnnotationMaximumTagsLength > 4096:
// ensure that the configuration does not exceed the respective column size
return fmt.Errorf("[annotations.tags_length] configuration exceeds the maximum allowed (4096)")
case cfg.AnnotationMaximumTagsLength > 500:
cfg.Logger.Info("[annotations.tags_length] has been increased from its default value; this may affect the performance", "tagLength", cfg.AnnotationMaximumTagsLength)
case cfg.AnnotationMaximumTagsLength < 500:
cfg.Logger.Warn("[annotations.tags_length] is too low; the minimum allowed (500) is enforced")
cfg.AnnotationMaximumTagsLength = 500
}
dashboardAnnotation := cfg.Raw.Section("annotations.dashboard")
apiIAnnotation := cfg.Raw.Section("annotations.api")
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),
}
}
alertingAnnotations := cfg.Raw.Section("unified_alerting.state_history.annotations")
if alertingAnnotations.Key("max_age").Value() == "" && section.Key("max_annotations_to_keep").Value() == "" {
// Although this section is not documented anymore, we decided to keep it to avoid potential data-loss when user upgrades Grafana and does not change the setting.
// TODO delete some time after Grafana 11.
alertingSection := cfg.Raw.Section("alerting")
cleanup := newAnnotationCleanupSettings(alertingSection, "max_annotation_age")
if cleanup.MaxCount > 0 || cleanup.MaxAge > 0 {
cfg.Logger.Warn("settings 'max_annotations_to_keep' and 'max_annotation_age' in section [alerting] are deprecated. Please use settings 'max_annotations_to_keep' and 'max_age' in section [unified_alerting.state_history.annotations]")
}
cfg.AlertingAnnotationCleanupSetting = cleanup
} else {
cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingAnnotations, "max_age")
}
cfg.DashboardAnnotationCleanupSettings = newAnnotationCleanupSettings(dashboardAnnotation, "max_age")
cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age")
return nil
}
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 (cfg *Cfg) applyCommandLineDefaultProperties(props map[string]string, file *ini.File) {
cfg.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)
cfg.appliedCommandLineProperties = append(cfg.appliedCommandLineProperties,
fmt.Sprintf("%s=%s", keyString, RedactedValue(keyString, value)))
}
}
}
}
func (cfg *Cfg) 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 {
cfg.appliedCommandLineProperties = append(cfg.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)
}
// micro-optimization since we don't need to share this ini file. In
// general, prefer to leave this flag as true as it is by default to prevent
// data races
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())
}
}
cfg.configFiles = append(cfg.configFiles, configFile)
return nil
}
func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
// load config defaults
defaultConfigFile := path.Join(cfg.HomePath, "conf/defaults.ini")
cfg.configFiles = append(cfg.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
}
// command line props
commandLineProps := cfg.getCommandLineProperties(args.Args)
// load default overrides
cfg.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 = cfg.applyEnvVariableOverrides(parsedFile)
if err != nil {
return nil, err
}
// apply command line overrides
cfg.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, cfg.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
return
}
var err error
cfg.HomePath, err = filepath.Abs(".")
if err != nil {
panic(err)
}
// 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, "../")
}
}
var skipStaticRootValidation = false
func NewCfg() *Cfg {
return &Cfg{
Env: Dev,
Target: []string{"all"},
Logger: log.New("settings"),
Raw: ini.Empty(),
Azure: &azsettings.AzureSettings{},
// Avoid nil pointer
IsFeatureToggleEnabled: func(_ string) bool {
return false
},
}
}
// Deprecated: Avoid using IsFeatureToggleEnabled from settings. If you need to access
// feature flags, read them from the FeatureToggle (or FeatureManager) interface
func NewCfgWithFeatures(features func(string) bool) *Cfg {
cfg := NewCfg()
cfg.IsFeatureToggleEnabled = features
return cfg
}
func NewCfgFromArgs(args CommandLineArgs) (*Cfg, error) {
cfg := NewCfg()
if err := cfg.Load(args); err != nil {
return nil, err
}
return cfg, nil
}
// NewCfgFromBytes specialized function to create a new Cfg from bytes (INI file).
func NewCfgFromBytes(bytes []byte) (*Cfg, error) {
parsedFile, err := ini.Load(bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse bytes as INI file: %w", err)
}
return NewCfgFromINIFile(parsedFile)
}
// NewCfgFromINIFile specialized function to create a new Cfg from an ini.File.
func NewCfgFromINIFile(iniFile *ini.File) (*Cfg, error) {
cfg := NewCfg()
if err := cfg.parseINIFile(iniFile); err != nil {
return nil, fmt.Errorf("failed to parse setting from INI file: %w", err)
}
return cfg, nil
}
func (cfg *Cfg) validateStaticRootPath() error {
if skipStaticRootValidation {
return nil
}
if _, err := os.Stat(path.Join(cfg.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(cfg.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
}
err = cfg.parseINIFile(iniFile)
if err != nil {
return err
}
cfg.LogConfigSources()
return nil
}
// nolint:gocyclo
func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
cfg.Raw = iniFile
cfg.BuildVersion = BuildVersion
cfg.BuildCommit = BuildCommit
cfg.EnterpriseBuildCommit = EnterpriseBuildCommit
cfg.BuildStamp = BuildStamp
cfg.BuildBranch = BuildBranch
cfg.IsEnterprise = IsEnterprise
cfg.Packaging = Packaging
cfg.ErrTemplateName = "error"
Target := valueAsString(iniFile.Section(""), "target", "all")
if Target != "" {
cfg.Target = util.SplitString(Target)
}
cfg.Env = valueAsString(iniFile.Section(""), "app_mode", "development")
cfg.StackID = valueAsString(iniFile.Section("environment"), "stack_id", "")
cfg.Slug = valueAsString(iniFile.Section("environment"), "stack_slug", "")
cfg.LocalFileSystemAvailable = iniFile.Section("environment").Key("local_file_system_available").MustBool(true)
//nolint:staticcheck
cfg.ForceMigration = iniFile.Section("").Key("force_migration").MustBool(false)
cfg.InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name")
plugins := valueAsString(iniFile.Section("paths"), "plugins", "")
cfg.PluginsPath = makeAbsolute(plugins, cfg.HomePath)
cfg.BundledPluginsPath = makeAbsolute("plugins-bundled", cfg.HomePath)
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.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
}
if err := readGRPCServerSettings(cfg, iniFile); err != nil {
return err
}
// read dashboard settings
dashboards := iniFile.Section("dashboards")
cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
cfg.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 := readServiceAccountSettings(iniFile, cfg); err != nil {
return err
}
if err := readAuthSettings(iniFile, cfg); err != nil {
return err
}
readOAuth2ServerSettings(cfg)
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)
cfg.MetricsIncludeTeamLabel = iniFile.Section("metrics").Key("include_team_label").MustBool(false)
cfg.MetricsTotalStatsIntervalSeconds = iniFile.Section("metrics").Key("total_stats_collector_interval_seconds").MustInt(1800)
analytics := iniFile.Section("analytics")
cfg.CheckForGrafanaUpdates = analytics.Key("check_for_updates").MustBool(true)
cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true)
cfg.GoogleAnalyticsID = analytics.Key("google_analytics_ua_id").String()
cfg.GoogleAnalytics4ID = analytics.Key("google_analytics_4_id").String()
cfg.GoogleAnalytics4SendManualPageViews = analytics.Key("google_analytics_4_send_manual_page_views").MustBool(false)
cfg.GoogleTagManagerID = analytics.Key("google_tag_manager_id").String()
cfg.RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
cfg.RudderstackDataPlaneURL = analytics.Key("rudderstack_data_plane_url").String()
cfg.RudderstackSDKURL = analytics.Key("rudderstack_sdk_url").String()
cfg.RudderstackConfigURL = analytics.Key("rudderstack_config_url").String()
cfg.RudderstackIntegrationsURL = analytics.Key("rudderstack_integrations_url").String()
cfg.IntercomSecret = analytics.Key("intercom_secret").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 := cfg.readAlertingSettings(iniFile); err != nil {
return err
}
explore := iniFile.Section("explore")
cfg.ExploreEnabled = explore.Key("enabled").MustBool(true)
help := iniFile.Section("help")
cfg.HelpEnabled = help.Key("enabled").MustBool(true)
profile := iniFile.Section("profile")
cfg.ProfileEnabled = profile.Key("enabled").MustBool(true)
news := iniFile.Section("news")
cfg.NewsFeedEnabled = news.Key("news_feed_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
}
// nolint:staticcheck
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.readSAMLConfig()
cfg.readLDAPConfig()
cfg.handleAWSConfig()
cfg.readAzureSettings()
cfg.readAuthJWTSettings()
cfg.readAuthProxySettings()
cfg.readSessionConfig()
if err := cfg.readSmtpSettings(); err != nil {
return err
}
if err := cfg.readAnnotationSettings(); err != nil {
return err
}
cfg.readQuotaSettings()
cfg.readExpressionsSettings()
if err := cfg.readGrafanaEnvironmentMetrics(); err != nil {
return err
}
cfg.readDataSourcesSettings()
cfg.readDataSourceSecuritySettings()
cfg.readSqlDataSourceSettings()
cfg.Storage = readStorageSettings(iniFile)
cfg.Search = readSearchSettings(iniFile)
var err error
cfg.SecureSocksDSProxy, err = readSecureSocksDSProxySettings(iniFile)
if err != nil {
// if the proxy is misconfigured, disable it rather than crashing
cfg.SecureSocksDSProxy.Enabled = false
cfg.Logger.Error("secure_socks_datasource_proxy unable to start up", "err", err.Error())
}
if cfg.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
cfg.GrafanaComAPIURL = valueAsString(iniFile.Section("grafana_com"), "api_url", grafanaComUrl+"/api")
imageUploadingSection := iniFile.Section("external_image_storage")
cfg.ImageUploadProvider = valueAsString(imageUploadingSection, "provider", "")
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", "")
prefix := valueAsString(cacheServer, "prefix", "")
encryption := cacheServer.Key("encryption").MustBool(false)
cfg.RemoteCacheOptions = &RemoteCacheOptions{
Name: dbName,
ConnStr: connStr,
Prefix: prefix,
Encryption: encryption,
}
geomapSection := iniFile.Section("geomap")
basemapJSON := valueAsString(geomapSection, "default_baselayer_config", "")
if basemapJSON != "" {
layer := make(map[string]any)
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.readGrafanaJavascriptAgentConfig()
if err := cfg.readLiveSettings(iniFile); err != nil {
return err
}
databaseSection := iniFile.Section("database")
cfg.DatabaseInstrumentQueries = databaseSection.Key("instrument_queries").MustBool(false)
logSection := iniFile.Section("log")
cfg.UserFacingDefaultError = logSection.Key("user_facing_default_error").MustString("please inspect Grafana server log for details")
cfg.readFeatureManagementConfig()
cfg.readPublicDashboardsSettings()
cfg.readCloudMigrationSettings()
// read experimental scopes settings.
scopesSection := iniFile.Section("scopes")
cfg.ScopesListScopesURL = scopesSection.Key("list_scopes_endpoint").MustString("")
cfg.ScopesListDashboardsURL = scopesSection.Key("list_dashboards_endpoint").MustString("")
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
Prefix string
Encryption bool
}
func (cfg *Cfg) readSAMLConfig() {
samlSec := cfg.Raw.Section("auth.saml")
cfg.SAMLAuthEnabled = samlSec.Key("enabled").MustBool(false)
cfg.SAMLSkipOrgRoleSync = samlSec.Key("skip_org_role_sync").MustBool(false)
cfg.SAMLRoleValuesGrafanaAdmin = samlSec.Key("role_values_grafana_admin").MustString("")
}
func (cfg *Cfg) readLDAPConfig() {
ldapSec := cfg.Raw.Section("auth.ldap")
cfg.LDAPConfigFilePath = ldapSec.Key("config_file").String()
cfg.LDAPSyncCron = ldapSec.Key("sync_cron").String()
cfg.LDAPAuthEnabled = ldapSec.Key("enabled").MustBool(false)
cfg.LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false)
cfg.LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
cfg.LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
}
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)
cfg.AWSExternalId = awsPluginSec.Key("external_id").Value()
cfg.AWSSessionDuration = awsPluginSec.Key("session_duration").Value()
cfg.AWSForwardSettingsPlugins = util.SplitString(awsPluginSec.Key("forward_settings_to_plugins").String())
// 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)
}
err = os.Setenv(awsds.ListMetricsPageLimitKeyName, strconv.Itoa(cfg.AWSListMetricsPageLimit))
if err != nil {
cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.ListMetricsPageLimitKeyName), err)
}
err = os.Setenv(awsds.GrafanaAssumeRoleExternalIdKeyName, cfg.AWSExternalId)
if err != nil {
cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.GrafanaAssumeRoleExternalIdKeyName), err)
}
err = os.Setenv(awsds.SessionDurationEnvVarKeyName, cfg.AWSSessionDuration)
if err != nil {
cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.SessionDurationEnvVarKeyName), 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, cfg.HomePath)
return log.ReadLoggingConfig(logModes, cfg.LogsPath, file)
}
func (cfg *Cfg) LogConfigSources() {
var text bytes.Buffer
for _, file := range cfg.configFiles {
cfg.Logger.Info("Config loaded from", "file", file)
}
if len(cfg.appliedCommandLineProperties) > 0 {
for _, prop := range cfg.appliedCommandLineProperties {
cfg.Logger.Info("Config overridden from command line", "arg", prop)
}
}
if len(cfg.appliedEnvOverrides) > 0 {
text.WriteString("\tEnvironment variables used:\n")
for _, prop := range cfg.appliedEnvOverrides {
cfg.Logger.Info("Config overridden from Environment variable", "var", prop)
}
}
cfg.Logger.Info("Target", "target", cfg.Target)
cfg.Logger.Info("Path Home", "path", cfg.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
env osutil.Env
}
// 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 := s.env.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
}
func (s *DynamicSection) KeysHash() map[string]string {
hash := s.section.KeysHash()
for k := range hash {
envKey := EnvKey(s.section.Name(), k)
envValue := s.env.Getenv(envKey)
if len(envValue) > 0 {
hash[k] = envValue
}
}
return hash
}
// 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{
section: cfg.Raw.Section(s),
Logger: cfg.Logger,
env: osutil.RealEnv{},
}
}
func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
security := iniFile.Section("security")
cfg.SecretKey = valueAsString(security, "secret_key", "")
cfg.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.AngularSupportEnabled = security.Key("angular_support_enabled").MustBool(true)
cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false)
cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("")
cfg.CSPReportOnlyEnabled = security.Key("content_security_policy_report_only").MustBool(false)
cfg.CSPReportOnlyTemplate = security.Key("content_security_policy_report_only_template").MustString("")
disableFrontendSandboxForPlugins := security.Key("disable_frontend_sandbox_for_plugins").MustString("")
for _, plug := range strings.Split(disableFrontendSandboxForPlugins, ",") {
plug = strings.TrimSpace(plug)
cfg.DisableFrontendSandboxForPlugins = append(cfg.DisableFrontendSandboxForPlugins, plug)
}
if cfg.CSPEnabled && cfg.CSPTemplate == "" {
return fmt.Errorf("enabling content_security_policy requires a content_security_policy_template configuration")
}
if cfg.CSPReportOnlyEnabled && cfg.CSPReportOnlyTemplate == "" {
return fmt.Errorf("enabling content_security_policy_report_only requires a content_security_policy_report_only_template configuration")
}
// read data source proxy whitelist
cfg.DataProxyWhiteList = make(map[string]bool)
securityStr := valueAsString(security, "data_source_proxy_whitelist", "")
for _, hostAndIP := range util.SplitString(securityStr) {
cfg.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", "")
cfg.AdminEmail = valueAsString(security, "admin_email", fmt.Sprintf("%s@localhost", cfg.AdminUser))
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
}
cfg.OAuthAllowInsecureEmailLookup = auth.Key("oauth_allow_insecure_email_lookup").MustBool(false)
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
}
// Do not use
cfg.AuthConfigUIAdminAccess = auth.Key("config_ui_admin_access").MustBool(false)
cfg.DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
cfg.DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
// Deprecated
cfg.OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
if cfg.OAuthAutoLogin {
cfg.Logger.Warn("[Deprecated] The oauth_auto_login configuration setting is deprecated. Please use auto_login inside auth provider section instead.")
}
cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600)
cfg.SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "")
// Deprecated
cfg.OAuthSkipOrgRoleUpdateSync = auth.Key("oauth_skip_org_role_update_sync").MustBool(false)
if cfg.OAuthSkipOrgRoleUpdateSync {
cfg.Logger.Warn("[Deprecated] The oauth_skip_org_role_update_sync configuration setting is deprecated. Please use skip_org_role_sync inside the auth provider section instead.")
}
cfg.DisableLogin = auth.Key("disable_login").MustBool(false)
// SigV4
cfg.SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false)
cfg.SigV4VerboseLogging = auth.Key("sigv4_verbose_logging").MustBool(false)
// Azure Auth
cfg.AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false)
// ID response header
cfg.IDResponseHeaderEnabled = auth.Key("id_response_header_enabled").MustBool(false)
cfg.IDResponseHeaderPrefix = auth.Key("id_response_header_prefix").MustString("X-Grafana")
idHeaderNamespaces := util.SplitString(auth.Key("id_response_header_namespaces").MustString(""))
cfg.IDResponseHeaderNamespaces = make(map[string]struct{}, len(idHeaderNamespaces))
for _, namespace := range idHeaderNamespaces {
cfg.IDResponseHeaderNamespaces[namespace] = struct{}{}
}
// anonymous access
anonSection := iniFile.Section("auth.anonymous")
cfg.AnonymousEnabled = anonSection.Key("enabled").MustBool(false)
cfg.AnonymousOrgName = valueAsString(anonSection, "org_name", "")
cfg.AnonymousOrgRole = valueAsString(anonSection, "org_role", "")
cfg.AnonymousHideVersion = anonSection.Key("hide_version").MustBool(false)
cfg.AnonymousDeviceLimit = anonSection.Key("device_limit").MustInt64(0)
// basic auth
authBasic := iniFile.Section("auth.basic")
cfg.BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
cfg.BasicAuthStrongPasswordPolicy = authBasic.Key("password_policy").MustBool(false)
// Extended JWT auth
authExtendedJWT := cfg.SectionWithEnvOverrides("auth.extended_jwt")
cfg.ExtendedJWTAuthEnabled = authExtendedJWT.Key("enabled").MustBool(false)
cfg.ExtendedJWTExpectAudience = authExtendedJWT.Key("expect_audience").MustString("")
cfg.ExtendedJWTExpectIssuer = authExtendedJWT.Key("expect_issuer").MustString("")
// SSO Settings
ssoSettings := iniFile.Section("sso_settings")
cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute)
providers := ssoSettings.Key("configurable_providers").String()
cfg.SSOSettingsConfigurableProviders = make(map[string]bool)
for _, provider := range util.SplitString(providers) {
cfg.SSOSettingsConfigurableProviders[provider] = true
}
return nil
}
func readAccessControlSettings(iniFile *ini.File, cfg *Cfg) {
rbac := iniFile.Section("rbac")
cfg.RBACPermissionCache = rbac.Key("permission_cache").MustBool(true)
cfg.RBACPermissionValidationEnabled = rbac.Key("permission_validation_enabled").MustBool(false)
cfg.RBACResetBasicRoles = rbac.Key("reset_basic_roles").MustBool(false)
cfg.RBACSingleOrganization = rbac.Key("single_organization").MustBool(false)
}
func readOAuth2ServerSettings(cfg *Cfg) {
oauth2Srv := cfg.SectionWithEnvOverrides("oauth2_server")
cfg.OAuth2ServerEnabled = oauth2Srv.Key("enabled").MustBool(false)
cfg.OAuth2ServerGeneratedKeyTypeForClient = strings.ToUpper(oauth2Srv.Key("generated_key_type_for_client").In("ECDSA", []string{"RSA", "ECDSA"}))
cfg.OAuth2ServerAccessTokenLifespan = oauth2Srv.Key("access_token_lifespan").MustDuration(time.Minute * 3)
}
func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
users := iniFile.Section("users")
cfg.AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
cfg.AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
cfg.AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
cfg.AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
cfg.AutoAssignOrgRole = users.Key("auto_assign_org_role").In(
string(roletype.RoleViewer), []string{
string(roletype.RoleNone),
string(roletype.RoleViewer),
string(roletype.RoleEditor),
string(roletype.RoleAdmin)})
cfg.VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
cfg.CaseInsensitiveLogin = users.Key("case_insensitive_login").MustBool(true)
cfg.LoginHint = valueAsString(users, "login_hint", "")
cfg.PasswordHint = valueAsString(users, "password_hint", "")
cfg.DefaultTheme = valueAsString(users, "default_theme", "")
cfg.DefaultLanguage = valueAsString(users, "default_language", "")
cfg.HomePage = valueAsString(users, "home_page", "")
cfg.ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
cfg.ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")
cfg.ExternalUserMngInfo = valueAsString(users, "external_manage_info", "")
cfg.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{}{}
}
}
verificationEmailMaxLifetimeVal := valueAsString(users, "verification_email_max_lifetime_duration", "1h")
verificationEmailMaxLifetimeDuration, err := gtime.ParseDuration(verificationEmailMaxLifetimeVal)
if err != nil {
return err
}
cfg.VerificationEmailMaxLifetime = verificationEmailMaxLifetimeDuration
return nil
}
func readServiceAccountSettings(iniFile *ini.File, cfg *Cfg) error {
serviceAccount := iniFile.Section("service_accounts")
cfg.SATokenExpirationDayLimit = serviceAccount.Key("token_expiration_day_limit").MustInt(-1)
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", "")
cfg.RendererAuthToken = valueAsString(renderSec, "renderer_token", "-")
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.RendererRenderKeyLifeTime = renderSec.Key("render_key_lifetime").MustDuration(5 * time.Minute)
cfg.RendererDefaultImageWidth = renderSec.Key("default_image_width").MustInt(1000)
cfg.RendererDefaultImageHeight = renderSec.Key("default_image_height").MustInt(500)
cfg.RendererDefaultImageScale = renderSec.Key("default_image_scale").MustFloat64(1)
cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv")
cfg.PDFsDir = filepath.Join(cfg.DataPath, "pdf")
return nil
}
func (cfg *Cfg) readAlertingSettings(iniFile *ini.File) error {
// This check is kept to prevent users that upgrade to Grafana 11 with the legacy alerting enabled. This should prevent them from accidentally upgrading without migration to Unified Alerting.
alerting := iniFile.Section("alerting")
enabled, err := alerting.Key("enabled").Bool()
if err == nil && enabled {
cfg.Logger.Error("Option '[alerting].enabled' cannot be true. Legacy Alerting is removed. It is no longer deployed, enhanced, or supported. Delete '[alerting].enabled' and use '[unified_alerting].enabled' to enable Grafana Alerting. For more information, refer to the documentation on upgrading to Grafana Alerting (https://grafana.com/docs/grafana/v10.4/alerting/set-up/migrating-alerts)")
return fmt.Errorf("invalid setting [alerting].enabled")
}
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 (cfg *Cfg) IsLegacyAlertingEnabled() bool {
return cfg.AlertingEnabled != nil && *(cfg.AlertingEnabled)
}
func readGRPCServerSettings(cfg *Cfg, iniFile *ini.File) error {
server := iniFile.Section("grpc_server")
errPrefix := "grpc_server:"
useTLS := server.Key("use_tls").MustBool(false)
certFile := server.Key("cert_file").String()
keyFile := server.Key("cert_key").String()
if useTLS {
serverCert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return fmt.Errorf("%s error loading X509 key pair: %w", errPrefix, err)
}
cfg.GRPCServerTLSConfig = &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,
}
}
cfg.GRPCServerNetwork = valueAsString(server, "network", "tcp")
cfg.GRPCServerAddress = valueAsString(server, "address", "")
switch cfg.GRPCServerNetwork {
case "unix":
if cfg.GRPCServerAddress != "" {
// Explicitly provided path for unix domain socket.
if stat, err := os.Stat(cfg.GRPCServerAddress); os.IsNotExist(err) {
// File does not exist - nice, nothing to do.
} else if err != nil {
return fmt.Errorf("%s error getting stat for a file: %s", errPrefix, cfg.GRPCServerAddress)
} else {
if stat.Mode()&fs.ModeSocket == 0 {
return fmt.Errorf("%s file %s already exists and is not a unix domain socket", errPrefix, cfg.GRPCServerAddress)
}
// Unix domain socket file, should be safe to remove.
err := os.Remove(cfg.GRPCServerAddress)
if err != nil {
return fmt.Errorf("%s can't remove unix socket file: %s", errPrefix, cfg.GRPCServerAddress)
}
}
} else {
// Use temporary file path for a unix domain socket.
tf, err := os.CreateTemp("", "gf_grpc_server_api")
if err != nil {
return fmt.Errorf("%s error creating tmp file: %v", errPrefix, err)
}
unixPath := tf.Name()
if err := tf.Close(); err != nil {
return fmt.Errorf("%s error closing tmp file: %v", errPrefix, err)
}
if err := os.Remove(unixPath); err != nil {
return fmt.Errorf("%s error removing tmp file: %v", errPrefix, err)
}
cfg.GRPCServerAddress = unixPath
}
case "tcp":
if cfg.GRPCServerAddress == "" {
cfg.GRPCServerAddress = "127.0.0.1:10000"
}
default:
return fmt.Errorf("%s unsupported network %s", errPrefix, cfg.GRPCServerNetwork)
}
return nil
}
func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error {
snapshots := iniFile.Section("snapshots")
cfg.SnapshotEnabled = snapshots.Key("enabled").MustBool(true)
cfg.ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "")
cfg.ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "")
cfg.ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
cfg.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
}
cfg.AppURL = AppUrl
cfg.AppSubURL = AppSubUrl
cfg.Protocol = HTTPScheme
cfg.ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false)
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.SocketGid = server.Key("socket_gid").MustInt(-1)
cfg.SocketMode = server.Key("socket_mode").MustInt(0660)
cfg.SocketPath = server.Key("socket").String()
}
cfg.MinTLSVersion = valueAsString(server, "min_tls_version", "TLS1.2")
if cfg.MinTLSVersion == "TLS1.0" || cfg.MinTLSVersion == "TLS1.1" {
return fmt.Errorf("TLS version not configured correctly:%v, allowed values are TLS1.2 and TLS1.3", cfg.MinTLSVersion)
}
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", "")
cfg.StaticRootPath = makeAbsolute(staticRoot, cfg.HomePath)
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)
headersSection := cfg.Raw.Section("server.custom_response_headers")
keys := headersSection.Keys()
cfg.CustomResponseHeaders = make(map[string]string, len(keys))
for _, key := range keys {
cfg.CustomResponseHeaders[key.Name()] = key.Value()
}
return nil
}
// GetContentDeliveryURL returns full content delivery URL with /<edition>/<version> added to URL
func (cfg *Cfg) GetContentDeliveryURL(prefix string) (string, error) {
if cfg.CDNRootURL == nil {
return "", nil
}
if cfg.BuildVersion == "" {
return "", errors.New("BuildVersion is not set")
}
url := *cfg.CDNRootURL
url.Path = path.Join(url.Path, prefix, cfg.BuildVersion)
return url.String() + "/", nil
}
func (cfg *Cfg) readDataSourcesSettings() {
datasources := cfg.Raw.Section("datasources")
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
cfg.ConcurrentQueryCount = datasources.Key("concurrent_query_count").MustInt(10)
}
func (cfg *Cfg) readDataSourceSecuritySettings() {
datasources := cfg.Raw.Section("datasources.ip_range_security")
cfg.IPRangeACEnabled = datasources.Key("enabled").MustBool(false)
cfg.IPRangeACSecretKey = datasources.Key("secret_key").MustString("")
if cfg.IPRangeACEnabled && cfg.IPRangeACSecretKey == "" {
cfg.Logger.Error("IP range access control is enabled but no secret key is set")
}
allowedURLString := datasources.Key("allow_list").MustString("")
for _, urlString := range util.SplitString(allowedURLString) {
allowedURL, err := url.Parse(urlString)
if err != nil {
cfg.Logger.Error("Error parsing allowed URL for IP range access control", "error", err)
continue
} else {
cfg.IPRangeACAllowedURLs = append(cfg.IPRangeACAllowedURLs, allowedURL)
}
}
}
func (cfg *Cfg) readSqlDataSourceSettings() {
sqlDatasources := cfg.Raw.Section("sql_datasources")
cfg.SqlDatasourceMaxOpenConnsDefault = sqlDatasources.Key("max_open_conns_default").MustInt(100)
cfg.SqlDatasourceMaxIdleConnsDefault = sqlDatasources.Key("max_idle_conns_default").MustInt(100)
cfg.SqlDatasourceMaxConnLifetimeDefault = sqlDatasources.Key("max_conn_lifetime_default").MustInt(14400)
}
func GetAllowedOriginGlobs(originPatterns []string) ([]glob.Glob, error) {
allowedOrigins := originPatterns
originGlobs := make([]glob.Glob, 0, len(allowedOrigins))
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")
cfg.LiveHAEnginePassword = section.Key("ha_engine_password").MustString("")
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
}
func (cfg *Cfg) readPublicDashboardsSettings() {
publicDashboards := cfg.Raw.Section("public_dashboards")
cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true)
}
func (cfg *Cfg) readCloudMigrationSettings() {
cloudMigration := cfg.Raw.Section("cloud_migration")
cfg.CloudMigrationIsTarget = cloudMigration.Key("is_target").MustBool(false)
}