diff --git a/api/v4/source/system.yaml b/api/v4/source/system.yaml index c42c418dc61..9c51dac6359 100644 --- a/api/v4/source/system.yaml +++ b/api/v4/source/system.yaml @@ -333,6 +333,27 @@ ##### Permissions Must have `manage_system` permission. operationId: GetConfig + parameters: + - name: remove_masked + in: query + description: | + Remove masked values from the exported configuration. + + __Minimum server version__: 10.4.0 + required: false + schema: + type: boolean + default: false + - name: remove_defaults + in: query + description: | + Remove default values from the exported configuration. + + __Minimum server version__: 10.4.0 + required: false + schema: + type: string + default: false responses: "200": description: Configuration retrieval successful diff --git a/server/channels/api4/config.go b/server/channels/api4/config.go index 2056ef4a4ae..1d7abbadfe3 100644 --- a/server/channels/api4/config.go +++ b/server/channels/api4/config.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "reflect" + "strconv" "strings" "github.com/mattermost/mattermost/server/public/model" @@ -66,19 +67,31 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { return } + filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked")) + filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults")) auditRec.Success() w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + + filterOpts := model.ConfigFilterOptions{ + GetConfigOptions: model.GetConfigOptions{ + RemoveDefaults: filterDefaults, + RemoveMasked: filterMasked, + }, + } if c.App.Channels().License().IsCloud() { - js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable) - if jsonErr != nil { - c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) - return - } - w.Write(js) + filterOpts.TagFilters = append(filterOpts.TagFilters, model.FilterTag{ + TagType: model.ConfigAccessTagType, + TagName: model.ConfigAccessTagCloudRestrictable, + }) + } + m, err := model.FilterConfig(cfg, filterOpts) + if err != nil { + c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err) return } - if err := json.NewEncoder(w).Encode(cfg); err != nil { + + if err := json.NewEncoder(w).Encode(m); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } diff --git a/server/channels/api4/config_local.go b/server/channels/api4/config_local.go index c3959023945..39b704ab693 100644 --- a/server/channels/api4/config_local.go +++ b/server/channels/api4/config_local.go @@ -7,6 +7,7 @@ import ( "encoding/json" "net/http" "reflect" + "strconv" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -27,10 +28,24 @@ func (api *API) InitConfigLocal() { func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) { auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail) defer c.LogAuditRec(auditRec) - cfg := c.App.GetSanitizedConfig() + filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked")) + filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults")) + + filterOpts := model.ConfigFilterOptions{ + GetConfigOptions: model.GetConfigOptions{ + RemoveDefaults: filterDefaults, + RemoveMasked: filterMasked, + }, + } + + m, err := model.FilterConfig(c.App.Config(), filterOpts) + if err != nil { + c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } auditRec.Success() w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - if err := json.NewEncoder(w).Encode(cfg); err != nil { + if err := json.NewEncoder(w).Encode(m); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } diff --git a/server/channels/api4/config_test.go b/server/channels/api4/config_test.go index e2c34c2ae1b..7be9eef6a5e 100644 --- a/server/channels/api4/config_test.go +++ b/server/channels/api4/config_test.go @@ -30,8 +30,8 @@ func TestGetConfig(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) - th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { - cfg, _, err := client.GetConfig(context.Background()) + t.Run("Get config for system admin client", func(t *testing.T) { + cfg, _, err := th.SystemAdminClient.GetConfig(context.Background()) require.NoError(t, err) require.NotEqual(t, "", cfg.TeamSettings.SiteName) @@ -59,6 +59,14 @@ func TestGetConfig(t *testing.T) { require.FailNow(t, "did not sanitize properly") } }) + + t.Run("Get config for local client", func(t *testing.T) { + cfg, _, err := th.LocalClient.GetConfig(context.Background()) + require.NoError(t, err) + + require.NotEqual(t, model.FakeSetting, *cfg.SqlSettings.DataSource) + require.NotEqual(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt) + }) } func TestGetConfigWithAccessTag(t *testing.T) { diff --git a/server/cmd/mmctl/client/client.go b/server/cmd/mmctl/client/client.go index 9bc1fa922d6..55ef8635888 100644 --- a/server/cmd/mmctl/client/client.go +++ b/server/cmd/mmctl/client/client.go @@ -91,6 +91,7 @@ type Client interface { MoveCommand(ctx context.Context, teamID string, commandID string) (*model.Response, error) DeleteCommand(ctx context.Context, commandID string) (*model.Response, error) GetConfig(ctx context.Context) (*model.Config, *model.Response, error) + GetConfigWithOptions(ctx context.Context, options model.GetConfigOptions) (map[string]any, *model.Response, error) GetOldClientConfig(ctx context.Context, etag string) (map[string]string, *model.Response, error) UpdateConfig(context.Context, *model.Config) (*model.Config, *model.Response, error) PatchConfig(context.Context, *model.Config) (*model.Config, *model.Response, error) diff --git a/server/cmd/mmctl/commands/config.go b/server/cmd/mmctl/commands/config.go index 4a6e68e6eca..305072dedd4 100644 --- a/server/cmd/mmctl/commands/config.go +++ b/server/cmd/mmctl/commands/config.go @@ -120,6 +120,15 @@ var ConfigSubpathCmd = &cobra.Command{ RunE: configSubpathCmdF, } +var ConfigExportCmd = &cobra.Command{ + Use: "export", + Short: "Export the server configuration", + Long: "Export the server configuration in case you want to import somewhere else.", + Example: "config export --remove-masked --remove-defaults", + Args: cobra.NoArgs, + RunE: withClient(configExportCmdF), +} + func init() { ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value") @@ -128,6 +137,9 @@ func init() { ConfigSubpathCmd.Flags().StringP("path", "p", "", "path to update the assets with") _ = ConfigSubpathCmd.MarkFlagRequired("path") + ConfigExportCmd.Flags().Bool("remove-masked", true, "remove masked values from the exported configuration") + ConfigExportCmd.Flags().Bool("remove-defaults", false, "remove default values from the exported configuration") + ConfigCmd.AddCommand( ConfigGetCmd, ConfigSetCmd, @@ -138,6 +150,7 @@ func init() { ConfigReloadCmd, ConfigMigrateCmd, ConfigSubpathCmd, + ConfigExportCmd, ) RootCmd.AddCommand(ConfigCmd) } @@ -581,3 +594,22 @@ func cloudRestrictedR(t reflect.Type, path []string) bool { return false } + +func configExportCmdF(c client.Client, cmd *cobra.Command, _ []string) error { + removeDefaults, _ := cmd.Flags().GetBool("remove-defaults") + removeMasked, _ := cmd.Flags().GetBool("remove-masked") + config, _, err := c.GetConfigWithOptions(context.TODO(), model.GetConfigOptions{ + RemoveDefaults: removeDefaults, + RemoveMasked: removeMasked, + }) + if err != nil { + return err + } + + printer.SetSingle(true) + printer.SetFormat(printer.FormatJSON) + + printer.Print(config) + + return nil +} diff --git a/server/cmd/mmctl/commands/config_e2e_test.go b/server/cmd/mmctl/commands/config_e2e_test.go index a2c00dd56c8..5b5810ad7db 100644 --- a/server/cmd/mmctl/commands/config_e2e_test.go +++ b/server/cmd/mmctl/commands/config_e2e_test.go @@ -233,3 +233,93 @@ func (s *MmctlE2ETestSuite) TestConfigShowCmdF() { s.Require().Len(printer.GetErrorLines(), 0) }) } + +func (s *MmctlE2ETestSuite) TestConfigExportCmdF() { + s.SetupTestHelper().InitBasic() + + s.RunForSystemAdminAndLocal("Get config normally", func(c client.Client) { + printer.Clean() + + err := configExportCmdF(c, &cobra.Command{}, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Len(printer.GetErrorLines(), 0) + + m, ok := printer.GetLines()[0].(map[string]any) + s.Require().True(ok) + if c == s.th.LocalClient { + // filter config is used to convert the config to a map[string]any + // local client has unrestricted access to the config + expectedConfig, err2 := model.FilterConfig(s.th.App.Config(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}}) + s.Require().NoError(err2) + s.Require().Equal(expectedConfig, m) + } else { + // filter config is used to convert the config to a map[string]any + // system admin client has restricted access to the config + expectedConfig, err2 := model.FilterConfig(s.th.App.GetSanitizedConfig(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}}) + s.Require().NoError(err2) + s.Require().Equal(expectedConfig, m) + } + }) + + s.Run("Should remove masked values for system admin client", func() { + printer.Clean() + + exportCmd := &cobra.Command{} + exportCmd.Flags().Bool("remove-masked", true, "") + err := configExportCmdF(s.th.SystemAdminClient, exportCmd, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + m, ok := printer.GetLines()[0].(map[string]any) + s.Require().True(ok) + ss, ok := m["SqlSettings"].(map[string]any) + s.Require().True(ok) + _, ok = ss["DataSource"] + s.Require().False(ok) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("Should retrieve configuration as-is with local client", func() { + printer.Clean() + + exportCmd := &cobra.Command{} + err := configExportCmdF(s.th.LocalClient, exportCmd, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + m, ok := printer.GetLines()[0].(map[string]any) + s.Require().True(ok) + ss, ok := m["SqlSettings"].(map[string]any) + s.Require().True(ok) + ds, ok := ss["DataSource"] + s.Require().True(ok) + cfg := s.th.App.Config() + s.Require().Equal(*cfg.SqlSettings.DataSource, ds) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.RunForSystemAdminAndLocal("Should remove default values", func(c client.Client) { + printer.Clean() + + exportCmd := &cobra.Command{} + exportCmd.Flags().Bool("remove-defaults", true, "") + err := configExportCmdF(c, exportCmd, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + m, ok := printer.GetLines()[0].(map[string]any) + s.Require().True(ok) + ss, ok := m["TeamSettings"].(map[string]any) + s.Require().True(ok) + _, ok = ss["MaxUsersPerTeam"] // it's not being changed by the test suite + s.Require().False(ok) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("Get config value for a given key without permissions", func() { + printer.Clean() + + err := configExportCmdF(s.th.Client, &cobra.Command{}, nil) + s.Require().NotNil(err) + s.Require().Len(printer.GetLines(), 0) + s.Require().Len(printer.GetErrorLines(), 0) + }) +} diff --git a/server/cmd/mmctl/commands/config_test.go b/server/cmd/mmctl/commands/config_test.go index 9c3ee45f558..66c05352318 100644 --- a/server/cmd/mmctl/commands/config_test.go +++ b/server/cmd/mmctl/commands/config_test.go @@ -1002,3 +1002,26 @@ func TestSetConfigValue(t *testing.T) { assert.Equal(t, tc.expectedConfig, tc.config, name) } } + +func (s *MmctlUnitTestSuite) TestConfigExportCmd() { + s.Run("Should get the config as-is", func() { + // there is not much to test as the config is returned as-is + // adding a test to make sure future changes are not breaking this + printer.Clean() + + s.client. + EXPECT(). + GetConfigWithOptions(context.TODO(), model.GetConfigOptions{}). + Return(map[string]any{ + "SqlSettings": map[string]any{ + "DriverName": "postgres", + }, + }, &model.Response{}, nil). + Times(1) + + err := configExportCmdF(s.client, &cobra.Command{}, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Len(printer.GetErrorLines(), 0) + }) +} diff --git a/server/cmd/mmctl/docs/mmctl_config.rst b/server/cmd/mmctl/docs/mmctl_config.rst index f02379ea69a..9942c6a53cf 100644 --- a/server/cmd/mmctl/docs/mmctl_config.rst +++ b/server/cmd/mmctl/docs/mmctl_config.rst @@ -38,6 +38,7 @@ SEE ALSO * `mmctl `_ - Remote client for the Open Source, self-hosted Slack-alternative * `mmctl config edit `_ - Edit the config +* `mmctl config export `_ - Export the server configuration * `mmctl config get `_ - Get config setting * `mmctl config migrate `_ - Migrate existing config between backends * `mmctl config patch `_ - Patch the config diff --git a/server/cmd/mmctl/docs/mmctl_config_export.rst b/server/cmd/mmctl/docs/mmctl_config_export.rst new file mode 100644 index 00000000000..0f898b1dff5 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_config_export.rst @@ -0,0 +1,53 @@ +.. _mmctl_config_export: + +mmctl config export +------------------- + +Export the server configuration + +Synopsis +~~~~~~~~ + + +Export the server configuration in case you want to import somewhere else. + +:: + + mmctl config export [flags] + +Examples +~~~~~~~~ + +:: + + config export --remove-masked --remove-defaults + +Options +~~~~~~~ + +:: + + -h, --help help for export + --remove-defaults remove default values from the exported configuration + --remove-masked remove masked values from the exported configuration (default true) + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl config `_ - Configuration + diff --git a/server/cmd/mmctl/mocks/client_mock.go b/server/cmd/mmctl/mocks/client_mock.go index cd23d7ce593..99ea8eee8aa 100644 --- a/server/cmd/mmctl/mocks/client_mock.go +++ b/server/cmd/mmctl/mocks/client_mock.go @@ -763,6 +763,22 @@ func (mr *MockClientMockRecorder) GetConfig(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockClient)(nil).GetConfig), arg0) } +// GetConfigWithOptions mocks base method. +func (m *MockClient) GetConfigWithOptions(arg0 context.Context, arg1 model.GetConfigOptions) (map[string]interface{}, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfigWithOptions", arg0, arg1) + ret0, _ := ret[0].(map[string]interface{}) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetConfigWithOptions indicates an expected call of GetConfigWithOptions. +func (mr *MockClientMockRecorder) GetConfigWithOptions(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigWithOptions", reflect.TypeOf((*MockClient)(nil).GetConfigWithOptions), arg0, arg1) +} + // GetDeletedChannelsForTeam mocks base method. func (m *MockClient) GetDeletedChannelsForTeam(arg0 context.Context, arg1 string, arg2, arg3 int, arg4 string) ([]*model.Channel, *model.Response, error) { m.ctrl.T.Helper() diff --git a/server/i18n/en.json b/server/i18n/en.json index 80c04895599..0987478696e 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2132,6 +2132,10 @@ "id": "api.file.write_file.app_error", "translation": "Unable to write the file." }, + { + "id": "api.filter_config_error", + "translation": "Unable to filter the configuration." + }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "OnlyThreads and OnlyTotals parameters to getThreadsForUser are mutually exclusive" diff --git a/server/public/model/client4.go b/server/public/model/client4.go index 27ffe46da82..0e3fc34538f 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -4886,6 +4886,30 @@ func (c *Client4) GetConfig(ctx context.Context) (*Config, *Response, error) { return cfg, BuildResponse(r), d.Decode(&cfg) } +// GetConfig will retrieve the server config with some sanitized items. +func (c *Client4) GetConfigWithOptions(ctx context.Context, options GetConfigOptions) (map[string]any, *Response, error) { + v := url.Values{} + if options.RemoveDefaults { + v.Set("remove_defaults", "true") + } + if options.RemoveMasked { + v.Set("remove_masked", "true") + } + url := c.configRoute() + if len(v) > 0 { + url += "?" + v.Encode() + } + + r, err := c.DoAPIGet(ctx, url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var cfg map[string]any + return cfg, BuildResponse(r), json.NewDecoder(r.Body).Decode(&cfg) +} + // ReloadConfig will reload the server configuration. func (c *Client4) ReloadConfig(ctx context.Context) (*Response, error) { r, err := c.DoAPIPost(ctx, c.configRoute()+"/reload", "") diff --git a/server/public/model/config.go b/server/public/model/config.go index 9fd44cc6c7d..ee882a9cfbf 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -4576,6 +4576,66 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) { o.PluginSettings.Sanitize(pluginManifests) } +type FilterTag struct { + TagType string + TagName string +} + +type ConfigFilterOptions struct { + GetConfigOptions + TagFilters []FilterTag +} + +type GetConfigOptions struct { + RemoveMasked bool + RemoveDefaults bool +} + +// FilterConfig returns a map[string]any representation of the configuration. +// Also, the function can filter the configuration by the options passed +// in the argument. The options are used to remove the default values, the masked +// values and to filter the configuration by the tags passed in the TagFilters. +func FilterConfig(cfg *Config, opts ConfigFilterOptions) (map[string]any, error) { + if cfg == nil { + return nil, nil + } + + defaultCfg := &Config{} + defaultCfg.SetDefaults() + + filteredCfg, err := cfg.StringMap() + if err != nil { + return nil, err + } + + filteredDefaultCfg, err := defaultCfg.StringMap() + if err != nil { + return nil, err + } + + for i := range opts.TagFilters { + filteredCfg = structToMapFilteredByTag(filteredCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName) + filteredDefaultCfg = structToMapFilteredByTag(defaultCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName) + } + + if opts.RemoveDefaults { + filteredCfg = stringMapDiff(filteredCfg, filteredDefaultCfg) + } + + if opts.RemoveMasked { + removeFakeSettings(filteredCfg) + } + + // only apply this if we applied some filters + // the alternative is to remove empty maps and slices during the filters + // but having this in a separate step makes it easier to understand + if opts.RemoveDefaults || opts.RemoveMasked || len(opts.TagFilters) > 0 { + removeEmptyMapsAndSlices(filteredCfg) + } + + return filteredCfg, nil +} + // structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed // as argument func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any { @@ -4625,6 +4685,90 @@ func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any return out } +// removeEmptyMapsAndSlices removes all the empty maps and slices from a map +func removeEmptyMapsAndSlices(m map[string]any) { + for k, v := range m { + switch vt := v.(type) { + case map[string]any: + removeEmptyMapsAndSlices(vt) + if len(vt) == 0 { + delete(m, k) + } + case []any: + if len(vt) == 0 { + delete(m, k) + } + } + } +} + +// StringMap returns a map[string]any representation of the Config struct +func (o *Config) StringMap() (map[string]any, error) { + b, err := json.Marshal(o) + if err != nil { + return nil, err + } + + var result map[string]any + err = json.Unmarshal(b, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +// stringMapDiff returns the difference between two maps with string keys +func stringMapDiff(m1, m2 map[string]any) map[string]any { + result := make(map[string]any) + + for k, v := range m1 { + if _, ok := m2[k]; !ok { + result[k] = v // ideally this should be never reached + } + + if reflect.DeepEqual(v, m2[k]) { + continue + } + + switch v.(type) { + case map[string]any: + // this happens during the serialization of the struct to map + // so we can safely assume that the type is not matching, there + // is a difference in the values + casted, ok := m2[k].(map[string]any) + if !ok { + result[k] = v + continue + } + res := stringMapDiff(v.(map[string]any), casted) + if len(res) > 0 { + result[k] = res + } + default: + result[k] = v + } + } + + return result +} + +// removeFakeSettings removes all the fields that have the value of FakeSetting +// it's necessary to remove the fields that have been masked to be able to +// export the configuration (and make it importable) +func removeFakeSettings(m map[string]any) { + for k, v := range m { + switch vt := v.(type) { + case map[string]any: + removeFakeSettings(vt) + case string: + if v == FakeSetting { + delete(m, k) + } + } + } +} + func isTagPresent(tag string, tags []string) bool { for _, val := range tags { tagValue := strings.TrimSpace(val) diff --git a/server/public/model/config_test.go b/server/public/model/config_test.go index 1d3fc7c9f36..e599494284c 100644 --- a/server/public/model/config_test.go +++ b/server/public/model/config_test.go @@ -1968,3 +1968,124 @@ func TestConfigDefaultConnectedWorkspacesSettings(t *testing.T) { require.True(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService) }) } + +func TestFilterConfig(t *testing.T) { + t.Run("should clear default values", func(t *testing.T) { + cfg := &Config{} + cfg.SetDefaults() + + m, err := FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.Empty(t, m) + + cfg.ServiceSettings = ServiceSettings{ + EnableLocalMode: NewPointer(true), + } + + m, err = FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, m) + require.Equal(t, true, m["ServiceSettings"].(map[string]any)["EnableLocalMode"]) + }) + + t.Run("should clear masked config values", func(t *testing.T) { + cfg := &Config{} + cfg.SetDefaults() + + dsn := "somedb://user:password@localhost:5432/mattermost" + cfg.SqlSettings.DataSource = NewPointer(dsn) + + m, err := FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, m) + require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"]) + + cfg.Sanitize(nil) + m, err = FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, m) + require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"]) + + cfg.Sanitize(nil) + m, err = FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + RemoveMasked: true, + }, + }) + require.NoError(t, err) + require.Empty(t, m) + + cfg.SqlSettings.DriverName = NewPointer("mysql") + m, err = FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + RemoveMasked: true, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, m) + require.Equal(t, "mysql", m["SqlSettings"].(map[string]any)["DriverName"]) + }) + + t.Run("should not clear non primitive types", func(t *testing.T) { + cfg := &Config{} + cfg.SetDefaults() + + cfg.TeamSettings.ExperimentalDefaultChannels = []string{"ch-a", "ch-b"} + m, err := FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, m) + require.ElementsMatch(t, []string{"ch-a", "ch-b"}, m["TeamSettings"].(map[string]any)["ExperimentalDefaultChannels"]) + }) + + t.Run("should be able to handle nil values", func(t *testing.T) { + var cfg *Config + + m, err := FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.Empty(t, m) + }) + + t.Run("should be able to handle float64 values", func(t *testing.T) { + cfg := &Config{} + cfg.SetDefaults() + cfg.PluginSettings.Plugins = map[string]map[string]any{ + "com.mattermost.plugin-a": { + "setting": 1.0, + }, + } + + m, err := FilterConfig(cfg, ConfigFilterOptions{ + GetConfigOptions: GetConfigOptions{ + RemoveDefaults: true, + }, + }) + require.NoError(t, err) + require.Equal(t, 1.0, m["PluginSettings"].(map[string]any)["Plugins"].(map[string]any)["com.mattermost.plugin-a"].(map[string]any)["setting"]) + }) +}