From bc1a2b34b1f96d48465ff662f9b4751db1c900de Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Mon, 16 Mar 2026 07:01:26 +0100 Subject: [PATCH] keeps plugin config on reenablement (#35545) (#35581) * keeps plugin config on reenablement * fixes local config patch on plugin reenablement (cherry picked from commit c9a4092ac0a20351e3c2e0ac0cb593cc28b5bc0e) Co-authored-by: Carlos Garcia --- server/channels/api4/config.go | 5 +++ server/channels/api4/config_local.go | 5 +++ server/channels/api4/config_test.go | 66 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/server/channels/api4/config.go b/server/channels/api4/config.go index 40d8fbb099..a43def1796 100644 --- a/server/channels/api4/config.go +++ b/server/channels/api4/config.go @@ -347,6 +347,11 @@ func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) { c.App.HandleMessageExportConfig(cfg, appCfg) } + // Treating an empty plugins map as nil preserves the existing configs. + if len(cfg.PluginSettings.Plugins) == 0 { + cfg.PluginSettings.Plugins = nil + } + updatedCfg, err := config.Merge(appCfg, cfg, &utils.MergeConfig{ StructFieldFilter: filterFn, }) diff --git a/server/channels/api4/config_local.go b/server/channels/api4/config_local.go index 3def645a20..7894ca29e8 100644 --- a/server/channels/api4/config_local.go +++ b/server/channels/api4/config_local.go @@ -116,6 +116,11 @@ func localPatchConfig(c *Context, w http.ResponseWriter, r *http.Request) { c.App.HandleMessageExportConfig(cfg, appCfg) } + // Treating an empty plugins map as nil preserves the existing configs. + if len(cfg.PluginSettings.Plugins) == 0 { + cfg.PluginSettings.Plugins = nil + } + updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{ StructFieldFilter: filterFn, }) diff --git a/server/channels/api4/config_test.go b/server/channels/api4/config_test.go index 6238191013..2df0a072c5 100644 --- a/server/channels/api4/config_test.go +++ b/server/channels/api4/config_test.go @@ -950,6 +950,72 @@ func TestPatchConfig(t *testing.T) { _, _, err = th.SystemAdminClient.PatchConfig(context.Background(), &model.Config{}) require.NoError(t, err) }) + + t.Run("should preserve plugin configs when toggling plugin enable off then on", func(t *testing.T) { + // Have some plugin settings setup + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.Enable = model.NewPointer(true) + cfg.PluginSettings.Plugins = map[string]map[string]any{ + "com.example.oauth-plugin": { + "clientid": "test-client-id", + "clientsecret": "test-client-secret", + }, + } + }) + + // First PATCH: disable the plugin subsystem + disablePatch := &model.Config{} + disablePatch.PluginSettings.Enable = model.NewPointer(false) + disabledResponse, _, err := th.SystemAdminClient.PatchConfig(context.Background(), disablePatch) + require.NoError(t, err) + // The sanitized response returns an empty Plugins map when plugins are disabled + assert.Empty(t, disabledResponse.PluginSettings.Plugins) + + // Second PATCH: re-enable plugins using the response from the first PATCH + disabledResponse.PluginSettings.Enable = model.NewPointer(true) + _, _, err = th.SystemAdminClient.PatchConfig(context.Background(), &model.Config{ + PluginSettings: disabledResponse.PluginSettings, + }) + require.NoError(t, err) + + // Plugin configs must survive the round-trip unchanged + storedCfg := th.App.Config() + require.Contains(t, storedCfg.PluginSettings.Plugins, "com.example.oauth-plugin") + assert.Equal(t, "test-client-id", storedCfg.PluginSettings.Plugins["com.example.oauth-plugin"]["clientid"]) + }) + + t.Run("local client should preserve plugin configs when toggling plugin enable off then on", func(t *testing.T) { + // Have some plugin settings setup + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.Enable = model.NewPointer(true) + cfg.PluginSettings.Plugins = map[string]map[string]any{ + "com.example.oauth-plugin": { + "clientid": "test-client-id", + "clientsecret": "test-client-secret", + }, + } + }) + + // First PATCH: disable the plugin subsystem + disablePatch := &model.Config{} + disablePatch.PluginSettings.Enable = model.NewPointer(false) + disabledResponse, _, err := th.LocalClient.PatchConfig(context.Background(), disablePatch) + require.NoError(t, err) + // The sanitized response returns an empty Plugins map when plugins are disabled + assert.Empty(t, disabledResponse.PluginSettings.Plugins) + + // Second PATCH: re-enable plugins using the response from the first PATCH + disabledResponse.PluginSettings.Enable = model.NewPointer(true) + _, _, err = th.LocalClient.PatchConfig(context.Background(), &model.Config{ + PluginSettings: disabledResponse.PluginSettings, + }) + require.NoError(t, err) + + // Plugin configs must survive the round-trip unchanged + storedCfg := th.App.Config() + require.Contains(t, storedCfg.PluginSettings.Plugins, "com.example.oauth-plugin") + assert.Equal(t, "test-client-id", storedCfg.PluginSettings.Plugins["com.example.oauth-plugin"]["clientid"]) + }) } func TestMigrateConfig(t *testing.T) {