diff --git a/rest/config_flags.go b/rest/config_flags.go index c53b32c635..1b1b3594c3 100644 --- a/rest/config_flags.go +++ b/rest/config_flags.go @@ -132,6 +132,7 @@ func registerConfigFlags(config *StartupConfig, fs *flag.FlagSet) map[string]con "unsupported.serverless.enabled": {&config.Unsupported.Serverless.Enabled, fs.Bool("unsupported.serverless.enabled", false, "Settings for running Sync Gateway in serverless mode.")}, "unsupported.serverless.min_config_fetch_interval": {&config.Unsupported.Serverless.MinConfigFetchInterval, fs.String("unsupported.serverless.min_config_fetch_interval", "", "How long to cache configs fetched from the buckets for. This cache is used for requested databases that SG does not know about.")}, "unsupported.use_xattr_config": {&config.Unsupported.UseXattrConfig, fs.Bool("unsupported.use_xattr_config", false, "Store database configurations in system xattrs")}, + "unsupported.allow_dbconfig_env_vars": {&config.Unsupported.AllowDbConfigEnvVars, fs.Bool("unsupported.allow_dbconfig_env_vars", true, "Can be set to false to skip environment variable expansion in database configs")}, "unsupported.user_queries": {&config.Unsupported.UserQueries, fs.Bool("unsupported.user_queries", false, "Whether user-query APIs are enabled")}, diff --git a/rest/config_startup.go b/rest/config_startup.go index cbf4efd8c8..e761ab05de 100644 --- a/rest/config_startup.go +++ b/rest/config_startup.go @@ -63,6 +63,7 @@ func DefaultStartupConfig(defaultLogFilePath string) StartupConfig { Enabled: base.BoolPtr(false), MinConfigFetchInterval: base.NewConfigDuration(DefaultMinConfigFetchInterval), }, + AllowDbConfigEnvVars: base.BoolPtr(true), }, MaxFileDescriptors: DefaultMaxFileDescriptors, } @@ -141,12 +142,13 @@ type ReplicatorConfig struct { } type UnsupportedConfig struct { - StatsLogFrequency *base.ConfigDuration `json:"stats_log_frequency,omitempty" help:"How often should stats be written to stats logs"` - UseStdlibJSON *bool `json:"use_stdlib_json,omitempty" help:"Bypass the jsoniter package and use Go's stdlib instead"` - Serverless ServerlessConfig `json:"serverless,omitempty"` - HTTP2 *HTTP2Config `json:"http2,omitempty"` - UserQueries *bool `json:"user_queries,omitempty" help:"Feature flag for user N1QL/JS/GraphQL queries"` - UseXattrConfig *bool `json:"use_xattr_config,omitempty" help:"Store database configurations in system xattrs"` + StatsLogFrequency *base.ConfigDuration `json:"stats_log_frequency,omitempty" help:"How often should stats be written to stats logs"` + UseStdlibJSON *bool `json:"use_stdlib_json,omitempty" help:"Bypass the jsoniter package and use Go's stdlib instead"` + Serverless ServerlessConfig `json:"serverless,omitempty"` + HTTP2 *HTTP2Config `json:"http2,omitempty"` + UserQueries *bool `json:"user_queries,omitempty" help:"Feature flag for user N1QL/JS/GraphQL queries"` + UseXattrConfig *bool `json:"use_xattr_config,omitempty" help:"Store database configurations in system xattrs"` + AllowDbConfigEnvVars *bool `json:"allow_dbconfig_env_vars,omitempty" help:"Can be set to false to skip environment variable expansion in database configs"` } type ServerlessConfig struct { diff --git a/rest/config_test.go b/rest/config_test.go index e13db14405..64c902562a 100644 --- a/rest/config_test.go +++ b/rest/config_test.go @@ -1306,6 +1306,86 @@ func TestExpandEnv(t *testing.T) { } } +// TestDbConfigEnvVarsToggle ensures that AllowDbConfigEnvVars toggles the ability to use env vars in db config. +func TestDbConfigEnvVarsToggle(t *testing.T) { + // Set up an env var with a secret value and use it as a channel name to assert its value. + const ( + varName = "SECRET_SG_TEST_VAR" + secretVal = "secret" + defaultVal = "substitute" + ) + // ${SECRET_SG_TEST_VAR:-substitute} + unexpandedVal := fmt.Sprintf("${%s:-%s}", varName, defaultVal) + + tests := []struct { + allowDbConfigEnvVars *bool + setEnvVar bool + expectedChannel string + unexpectedChannels []string + }{ + { + allowDbConfigEnvVars: nil, // defaults to true - so use nil to check default handling + setEnvVar: true, + expectedChannel: secretVal, + unexpectedChannels: []string{defaultVal, unexpandedVal}, + }, + { + allowDbConfigEnvVars: base.BoolPtr(true), + setEnvVar: false, + expectedChannel: defaultVal, + unexpectedChannels: []string{secretVal, unexpandedVal}, + }, + { + allowDbConfigEnvVars: base.BoolPtr(false), + setEnvVar: true, + expectedChannel: unexpandedVal, + unexpectedChannels: []string{secretVal, defaultVal}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("allowDbConfigEnvVars=%v_setEnvVar=%v", test.allowDbConfigEnvVars, test.setEnvVar), func(t *testing.T) { + rt := NewRestTesterDefaultCollection(t, &RestTesterConfig{ + PersistentConfig: true, + allowDbConfigEnvVars: test.allowDbConfigEnvVars, + }) + defer rt.Close() + + if test.setEnvVar { + require.NoError(t, os.Setenv(varName, secretVal)) + t.Cleanup(func() { + require.NoError(t, os.Unsetenv(varName)) + }) + } + + dbc := rt.NewDbConfig() + dbc.Sync = base.StringPtr(fmt.Sprintf( + "function(doc) {channel('%s');}", + unexpandedVal, + )) + + resp := rt.CreateDatabase("db", dbc) + AssertStatus(t, resp, http.StatusCreated) + + docID := "doc" + putDocResp := rt.PutDoc(docID, `{"foo":"bar"}`) + require.True(t, putDocResp.Ok) + + require.NoError(t, rt.WaitForPendingChanges()) + + // ensure doc is in expected channel and is not in the unexpected channels + changes, err := rt.WaitForChanges(1, "/{{.keyspace}}/_changes?filter=sync_gateway/bychannel&channels="+test.expectedChannel, "", true) + require.NoError(t, err) + changes.RequireDocIDs(t, []string{docID}) + + channels := strings.Join(test.unexpectedChannels, ",") + changes, err = rt.WaitForChanges(0, "/{{.keyspace}}/_changes?filter=sync_gateway/bychannel&channels="+channels, "", true) + require.NoError(t, err) + changes.RequireDocIDs(t, []string{}) + }) + } +} + // createTempFile creates a temporary file with the given content. func createTempFile(t *testing.T, content []byte) *os.File { file, err := os.CreateTemp("", "*-sync_gateway.conf") diff --git a/rest/handler.go b/rest/handler.go index 6adde9f816..99166b30e0 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -1109,10 +1109,13 @@ func (h *handler) readSanitizeJSON(val interface{}) error { } // Expand environment variables. - content, err = expandEnv(h.ctx(), content) - if err != nil { - return err + if base.BoolDefault(h.server.Config.Unsupported.AllowDbConfigEnvVars, true) { + content, err = expandEnv(h.ctx(), content) + if err != nil { + return err + } } + // Convert the back quotes into double-quotes, escapes literal // backslashes, newlines or double-quotes with backslashes. content = base.ConvertBackQuotedStrings(content) diff --git a/rest/utilities_testing.go b/rest/utilities_testing.go index ed091320ab..0d0ab88f79 100644 --- a/rest/utilities_testing.go +++ b/rest/utilities_testing.go @@ -70,6 +70,7 @@ type RestTesterConfig struct { serverless bool // Runs SG in serverless mode. Must be used in conjunction with persistent config collectionConfig collectionConfiguration numCollections int + allowDbConfigEnvVars *bool } type collectionConfiguration uint8 @@ -215,6 +216,7 @@ func (rt *RestTester) Bucket() base.Bucket { sc.Bootstrap.UseTLSServer = &rt.RestTesterConfig.useTLSServer sc.Bootstrap.ServerTLSSkipVerify = base.BoolPtr(base.TestTLSSkipVerify()) sc.Unsupported.Serverless.Enabled = &rt.serverless + sc.Unsupported.AllowDbConfigEnvVars = rt.RestTesterConfig.allowDbConfigEnvVars if rt.serverless { if !rt.PersistentConfig { rt.TB.Fatalf("Persistent config must be used when running in serverless mode")