diff --git a/server/plugin.go b/server/plugin.go index fb0d44c..c3a7fc9 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -1,6 +1,8 @@ package main import ( + "crypto/rand" + "encoding/base64" "fmt" "html/template" "net/http" @@ -101,6 +103,21 @@ func (p *Plugin) OnConfigurationChange() error { return err } + if len(configuration.EncryptionKey) != 32 { + newKey, err := generateRandomKey(32) + if err != nil { + config.Mattermost.LogError("Error generating encryption key.", "Error", err.Error()) + return err + } + configuration.EncryptionKey = newKey + config.Mattermost.LogInfo("Auto-generated missing Encryption Key.") + + if err := p.savePluginConfig(&configuration); err != nil { + config.Mattermost.LogError("Error saving auto-generated encryption key.", "Error", err.Error()) + return err + } + } + if err := configuration.IsValid(); err != nil { config.Mattermost.LogError("Error in Validating Configuration.", "Error", err.Error()) return err @@ -110,6 +127,29 @@ func (p *Plugin) OnConfigurationChange() error { return nil } +func generateRandomKey(length int) (string, error) { + // We need more bytes because base64 encoding expands the size + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + encoded := base64.URLEncoding.EncodeToString(bytes) + return encoded[:length], nil +} + +func (p *Plugin) savePluginConfig(configuration *config.Configuration) error { + configMap, err := configuration.ToMap() + if err != nil { + return errors.Wrap(err, "failed to convert config to map") + } + + if appErr := p.API.SavePluginConfig(configMap); appErr != nil { + return errors.Wrap(appErr, "failed to save plugin config") + } + + return nil +} + func (p *Plugin) setUpBotUser() error { botUserID, err := p.client.Bot.EnsureBot(&model.Bot{ Username: botUserName, diff --git a/server/plugin_test.go b/server/plugin_test.go index 66cf7c2..ea26011 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -8,6 +8,7 @@ import ( "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-plugin-confluence/server/config" ) @@ -58,3 +59,79 @@ func TestExecuteCommand(t *testing.T) { }) } } + +func TestGenerateRandomKey(t *testing.T) { + t.Run("generates key of correct length", func(t *testing.T) { + key, err := generateRandomKey(32) + require.NoError(t, err) + assert.Len(t, key, 32) + }) + + t.Run("generates unique keys", func(t *testing.T) { + key1, err := generateRandomKey(32) + require.NoError(t, err) + + key2, err := generateRandomKey(32) + require.NoError(t, err) + + assert.NotEqual(t, key1, key2, "Generated keys should be unique") + }) + + t.Run("generates different lengths", func(t *testing.T) { + key16, err := generateRandomKey(16) + require.NoError(t, err) + assert.Len(t, key16, 16) + + key64, err := generateRandomKey(64) + require.NoError(t, err) + assert.Len(t, key64, 64) + }) +} + +func TestOnConfigurationChange_AutoGenerateEncryptionKey(t *testing.T) { + t.Run("auto-generates encryption key when missing", func(t *testing.T) { + mockAPI := &plugintest.API{} + config.Mattermost = mockAPI + + p := &Plugin{} + p.SetAPI(mockAPI) + + // Configuration with valid secret but missing encryption key + mockAPI.On("LoadPluginConfiguration", mock.AnythingOfType("*config.Configuration")).Run(func(args mock.Arguments) { + cfg := args.Get(0).(*config.Configuration) + cfg.Secret = "12345678901234567890123456789012" // 32 chars + cfg.EncryptionKey = "" // Empty - should be auto-generated + }).Return(nil) + + mockAPI.On("LogInfo", "Auto-generated missing Encryption Key.").Return() + mockAPI.On("SavePluginConfig", mock.AnythingOfType("map[string]interface {}")).Return(nil) + + err := p.OnConfigurationChange() + require.NoError(t, err) + + // Verify SavePluginConfig was called (meaning key was generated and saved) + mockAPI.AssertCalled(t, "SavePluginConfig", mock.AnythingOfType("map[string]interface {}")) + }) + + t.Run("does not overwrite existing valid encryption key", func(t *testing.T) { + mockAPI := &plugintest.API{} + config.Mattermost = mockAPI + + p := &Plugin{} + p.SetAPI(mockAPI) + + existingKey := "abcdefghijklmnopqrstuvwxyz123456" // 32 chars + + mockAPI.On("LoadPluginConfiguration", mock.AnythingOfType("*config.Configuration")).Run(func(args mock.Arguments) { + cfg := args.Get(0).(*config.Configuration) + cfg.Secret = "12345678901234567890123456789012" + cfg.EncryptionKey = existingKey + }).Return(nil) + + err := p.OnConfigurationChange() + require.NoError(t, err) + + // SavePluginConfig should NOT be called since key is valid + mockAPI.AssertNotCalled(t, "SavePluginConfig", mock.Anything) + }) +}