mattermost/server/channels/api4/config_test.go
Jesse Hallam 84e267e9e8
[MM-66789] Restrict ImportSettings.Directory changes via API and add validation (#34653)
This change enhances security by preventing ImportSettings.Directory from being modified through the API and adds validation to prevent directory conflicts.

Changes:

- Restricted ImportSettings.Directory from being changed via API

- Added validation to prevent directory conflicts with plugin directory

- Added error message translation

- Updated and added comprehensive tests

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-01-06 16:30:07 +00:00

984 lines
36 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/config"
)
func TestGetConfig(t *testing.T) {
th := Setup(t)
client := th.Client
_, resp, err := client.GetConfig(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
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)
if *cfg.LdapSettings.BindPassword != model.FakeSetting && *cfg.LdapSettings.BindPassword != "" {
require.FailNow(t, "did not sanitize properly")
}
require.Equal(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly")
if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FakeSetting && *cfg.FileSettings.AmazonS3SecretAccessKey != "" {
require.FailNow(t, "did not sanitize properly")
}
if *cfg.EmailSettings.SMTPPassword != model.FakeSetting && *cfg.EmailSettings.SMTPPassword != "" {
require.FailNow(t, "did not sanitize properly")
}
if *cfg.GitLabSettings.Secret != model.FakeSetting && *cfg.GitLabSettings.Secret != "" {
require.FailNow(t, "did not sanitize properly")
}
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.DataSource, "did not sanitize properly")
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly")
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceReplicas) != 0 {
require.FailNow(t, "did not sanitize properly")
}
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 {
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) {
th := Setup(t)
// set some values so that we know they're not blank
mockVaryByHeader := model.NewId()
mockSupportEmail := model.NewId() + "@mattermost.com"
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.RateLimitSettings.VaryByHeader = mockVaryByHeader
cfg.SupportSettings.SupportEmail = &mockSupportEmail
})
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
// add read sysconsole environment config
th.AddPermissionToRole(t, model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
cfg, _, err := th.Client.GetConfig(context.Background())
require.NoError(t, err)
t.Run("Cannot read value without permission", func(t *testing.T) {
assert.Nil(t, cfg.SupportSettings.SupportEmail)
})
t.Run("Can read value with permission", func(t *testing.T) {
assert.Equal(t, mockVaryByHeader, cfg.RateLimitSettings.VaryByHeader)
})
t.Run("Contains Feature Flags", func(t *testing.T) {
assert.NotNil(t, cfg.FeatureFlags)
})
}
func TestGetConfigAnyFlagsAccess(t *testing.T) {
th := Setup(t)
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
_, resp, _ := th.Client.GetConfig(context.Background())
t.Run("Check permissions error with no sysconsole read permission", func(t *testing.T) {
CheckForbiddenStatus(t, resp)
})
// add read sysconsole environment config
th.AddPermissionToRole(t, model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
cfg, _, err := th.Client.GetConfig(context.Background())
require.NoError(t, err)
t.Run("Can read value with permission", func(t *testing.T) {
assert.NotNil(t, cfg.FeatureFlags)
assert.NotNil(t, cfg.ExperimentalSettings.RestrictSystemAdmin)
})
}
func TestReloadConfig(t *testing.T) {
th := Setup(t)
client := th.Client
t.Run("as system user", func(t *testing.T) {
resp, err := client.ReloadConfig(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.ReloadConfig(context.Background())
require.NoError(t, err)
}, "as system admin and local mode")
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := client.ReloadConfig(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestUpdateConfig(t *testing.T) {
th := Setup(t)
client := th.Client
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
_, resp, err := client.UpdateConfig(context.Background(), cfg)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
SiteName := th.App.Config().TeamSettings.SiteName
*cfg.TeamSettings.SiteName = "MyFancyName"
cfg, _, err = client.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName")
//Revert the change
cfg.TeamSettings.SiteName = SiteName
cfg, _, err = client.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName")
t.Run("Should set defaults for missing fields", func(t *testing.T) {
_, err = th.SystemAdminClient.DoAPIPut(context.Background(), "/config", "{}")
require.NoError(t, err)
})
t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) {
//Revert the change
badcfg := cfg.Clone()
badcfg.PasswordSettings.MinimumLength = model.NewPointer(4)
badcfg.PasswordSettings.MinimumLength = model.NewPointer(4)
_, resp, err = client.UpdateConfig(context.Background(), badcfg)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
})
})
t.Run("Ensure PluginSettings.EnableUploads settings are protected", func(t *testing.T) {
t.Run("sysadmin", func(t *testing.T) {
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
cfg.PluginSettings.EnableUploads = nil
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
})
t.Run("local mode", func(t *testing.T) {
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.NotEqual(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.NotEqual(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
cfg.PluginSettings.EnableUploads = nil
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
})
})
t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) {
t.Run("sysadmin", func(t *testing.T) {
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
cfg.PluginSettings.SignaturePublicKeyFiles = nil
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
})
t.Run("local mode", func(t *testing.T) {
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.NotEqual(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.NotEqual(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
cfg.PluginSettings.SignaturePublicKeyFiles = nil
cfg, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
})
})
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
oldURL := "hello.com"
newURL := "new.com"
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = false
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg2 := th.App.Config().Clone()
*cfg2.PluginSettings.MarketplaceURL = newURL
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, oldURL, *cfg2.PluginSettings.MarketplaceURL)
// Allowing uploads
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg2 = th.App.Config().Clone()
*cfg2.PluginSettings.MarketplaceURL = newURL
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, newURL, *cfg2.PluginSettings.MarketplaceURL)
})
t.Run("Should not be able to modify ComplianceSettings.Directory in cloud", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
defer func() {
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
cfg2 := th.App.Config().Clone()
*cfg2.ComplianceSettings.Directory = "hellodir"
_, resp, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Should not be able to modify ImportSettings.Directory", func(t *testing.T) {
t.Run("sysadmin", func(t *testing.T) {
oldDirectory := *th.App.Config().ImportSettings.Directory
cfg2 := th.App.Config().Clone()
*cfg2.ImportSettings.Directory = "./new-import-dir"
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
cfg2.ImportSettings.Directory = nil
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
})
t.Run("local mode", func(t *testing.T) {
oldDirectory := *th.App.Config().ImportSettings.Directory
cfg2 := th.App.Config().Clone()
newDirectory := "./new-import-dir"
*cfg2.ImportSettings.Directory = newDirectory
cfg2, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, newDirectory, *cfg2.ImportSettings.Directory)
assert.Equal(t, newDirectory, *th.App.Config().ImportSettings.Directory)
cfg2.ImportSettings.Directory = nil
cfg2, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg2)
require.NoError(t, err)
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
})
})
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
siteURL := cfg.ServiceSettings.SiteURL
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
nonEmptyURL := "http://localhost"
cfg.ServiceSettings.SiteURL = &nonEmptyURL
// Set the SiteURL
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
// Check that the Site URL can't be cleared
cfg.ServiceSettings.SiteURL = model.NewPointer("")
cfg, resp, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
// Check that the Site URL wasn't cleared
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
})
}
func TestGetConfigWithoutManageSystemPermission(t *testing.T) {
th := Setup(t)
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
t.Run("any sysconsole read permission provides config read access", func(t *testing.T) {
// forbidden by default
_, resp, err := th.Client.GetConfig(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// add any sysconsole read permission
th.AddPermissionToRole(t, model.SysconsoleReadPermissions[0].Id, model.SystemUserRoleId)
_, _, err = th.Client.GetConfig(context.Background())
// should be readable now
require.NoError(t, err)
})
}
func TestUpdateConfigWithoutManageSystemPermission(t *testing.T) {
th := Setup(t)
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
// add read sysconsole integrations config
th.AddPermissionToRole(t, model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
t.Run("sysconsole read permission does not provides config write access", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.Client.GetConfig(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.UpdateConfig(context.Background(), cfg)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("the wrong write permission does not grant access", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
originalValue := *cfg.ServiceSettings.AllowCorsFrom
// add the wrong write permission
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
// try update a config value allowed by sysconsole WRITE integrations
mockVal := model.NewId()
cfg.ServiceSettings.AllowCorsFrom = &mockVal
_, _, err = th.Client.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
// ensure the config setting was not updated
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, originalValue)
})
t.Run("config value is writeable by specific system console permission", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
// try update a config value allowed by sysconsole WRITE integrations
mockVal := model.NewId()
cfg.ServiceSettings.AllowCorsFrom = &mockVal
_, _, err = th.Client.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
// ensure the config setting was updated
cfg, _, err = th.Client.GetConfig(context.Background())
require.NoError(t, err)
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, mockVal)
})
}
func TestUpdateConfigMessageExportSpecialHandling(t *testing.T) {
th := Setup(t)
messageExportEnabled := *th.App.Config().MessageExportSettings.EnableExport
messageExportTimestamp := *th.App.Config().MessageExportSettings.ExportFromTimestamp
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = messageExportEnabled
*cfg.MessageExportSettings.ExportFromTimestamp = messageExportTimestamp
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = false
*cfg.MessageExportSettings.ExportFromTimestamp = int64(0)
})
// Turn it on, timestamp should be updated.
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = true
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.NotEqual(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Turn it off, timestamp should be cleared.
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = false
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Set a value from the config file.
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = false
*cfg.MessageExportSettings.ExportFromTimestamp = int64(12345)
})
// Turn it on, timestamp should *not* be updated.
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = true
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(12345), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Turn it off, timestamp should be cleared.
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = false
_, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
}
func TestUpdateConfigRestrictSystemAdmin(t *testing.T) {
th := Setup(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
t.Run("Restrict flag should be honored for sysadmin", func(t *testing.T) {
originalCfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
cfg := originalCfg.Clone()
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
returnedCfg, _, err := th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL)
actualCfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
require.Equal(t, returnedCfg, actualCfg)
})
t.Run("Restrict flag should be ignored by local mode", func(t *testing.T) {
originalCfg, _, err := th.LocalClient.GetConfig(context.Background())
require.NoError(t, err)
cfg := originalCfg.Clone()
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
returnedCfg, _, err := th.LocalClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
require.Equal(t, "http://example.com", *returnedCfg.ServiceSettings.SiteURL)
})
}
func TestUpdateConfigDiffInAuditRecord(t *testing.T) {
logFile, err := os.CreateTemp("", "adv.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED", "true")
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME", logFile.Name())
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED")
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME")
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptions(t, options)
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
timeoutVal := *cfg.ServiceSettings.ReadTimeout
cfg.ServiceSettings.ReadTimeout = model.NewPointer(timeoutVal + 1)
cfg, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg)
require.NoError(t, err)
defer th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.ReadTimeout = model.NewPointer(timeoutVal)
})
require.Equal(t, timeoutVal+1, *cfg.ServiceSettings.ReadTimeout)
// Forcing a flush before attempting to read log's content.
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
require.Contains(t, string(data),
fmt.Sprintf(`"config_diffs":[{"actual_val":%d,"base_val":%d,"path":"ServiceSettings.ReadTimeout"}`,
timeoutVal+1, timeoutVal))
}
func TestGetEnvironmentConfig(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI")
th := Setup(t)
t.Run("as system admin", func(t *testing.T) {
SystemAdminClient := th.SystemAdminClient
envConfig, _, err := SystemAdminClient.GetEnvironmentConfig(context.Background())
require.NoError(t, err)
serviceSettings, ok := envConfig["ServiceSettings"]
require.True(t, ok, "should've returned ServiceSettings")
serviceSettingsAsMap, ok := serviceSettings.(map[string]any)
require.True(t, ok, "should've returned ServiceSettings as a map")
siteURL, ok := serviceSettingsAsMap["SiteURL"]
require.True(t, ok, "should've returned ServiceSettings.SiteURL")
siteURLAsBool, ok := siteURL.(bool)
require.True(t, ok, "should've returned ServiceSettings.SiteURL as a boolean")
require.True(t, siteURLAsBool, "should've returned ServiceSettings.SiteURL as true")
enableCustomEmoji, ok := serviceSettingsAsMap["EnableCustomEmoji"]
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji")
enableCustomEmojiAsBool, ok := enableCustomEmoji.(bool)
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji as a boolean")
require.True(t, enableCustomEmojiAsBool, "should've returned ServiceSettings.EnableCustomEmoji as true")
_, ok = envConfig["TeamSettings"]
require.False(t, ok, "should not have returned TeamSettings")
})
t.Run("as team admin", func(t *testing.T) {
TeamAdminClient := th.CreateClient()
th.LoginTeamAdminWithClient(t, TeamAdminClient)
envConfig, _, err := TeamAdminClient.GetEnvironmentConfig(context.Background())
require.NoError(t, err)
require.Empty(t, envConfig)
})
t.Run("as regular user", func(t *testing.T) {
client := th.Client
envConfig, _, err := client.GetEnvironmentConfig(context.Background())
require.NoError(t, err)
require.Empty(t, envConfig)
})
t.Run("as not-regular user", func(t *testing.T) {
client := th.CreateClient()
_, resp, err := client.GetEnvironmentConfig(context.Background())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestGetClientConfig(t *testing.T) {
th := Setup(t)
testKey := "supersecretkey"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.GoogleDeveloperKey = testKey })
t.Run("with session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
})
client := th.Client
config, _, err := client.GetClientConfig(context.Background(), "")
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
require.Equal(t, testKey, config["GoogleDeveloperKey"])
})
t.Run("without session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
})
client := th.CreateClient()
config, _, err := client.GetClientConfig(context.Background(), "")
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
require.Empty(t, config["GoogleDeveloperKey"], "config should be missing developer key")
})
t.Run("format=old (backward compatibility)", func(t *testing.T) {
client := th.Client
resp, err := client.DoAPIGet(context.Background(), "/config/client?format=old", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var config map[string]string
err = json.NewDecoder(resp.Body).Decode(&config)
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
})
t.Run("format=junk (ignored)", func(t *testing.T) {
client := th.Client
resp, err := client.DoAPIGet(context.Background(), "/config/client?format=junk", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var config map[string]string
err = json.NewDecoder(resp.Body).Decode(&config)
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
})
}
func TestPatchConfig(t *testing.T) {
th := Setup(t)
// Ensure ConsoleLevel is set to DEBUG
config := model.Config{LogSettings: model.LogSettings{
ConsoleLevel: model.NewPointer("DEBUG"),
}}
_, _, err := th.SystemAdminClient.PatchConfig(context.Background(), &config)
require.NoError(t, err)
t.Run("config is missing", func(t *testing.T) {
_, response, err := th.Client.PatchConfig(context.Background(), nil)
require.Error(t, err)
CheckBadRequestStatus(t, response)
})
t.Run("user is not system admin", func(t *testing.T) {
_, response, err := th.Client.PatchConfig(context.Background(), &model.Config{})
require.Error(t, err)
CheckForbiddenStatus(t, response)
})
t.Run("should not update the restricted fields when restrict toggle is on for sysadmin", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
config := model.Config{LogSettings: model.LogSettings{
ConsoleLevel: model.NewPointer("INFO"),
}}
updatedConfig, _, _ := th.SystemAdminClient.PatchConfig(context.Background(), &config)
assert.Equal(t, "DEBUG", *updatedConfig.LogSettings.ConsoleLevel)
})
t.Run("should not bypass the restrict toggle if local client", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
config := model.Config{LogSettings: model.LogSettings{
ConsoleLevel: model.NewPointer("INFO"),
}}
oldConfig, _, _ := th.LocalClient.GetConfig(context.Background())
updatedConfig, _, _ := th.LocalClient.PatchConfig(context.Background(), &config)
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
// reset the config
_, _, err := th.LocalClient.UpdateConfig(context.Background(), oldConfig)
require.NoError(t, err)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
t.Run("check if config is valid", func(t *testing.T) {
config := model.Config{PasswordSettings: model.PasswordSettings{
MinimumLength: model.NewPointer(4),
}}
_, response, err := client.PatchConfig(context.Background(), &config)
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
assert.Error(t, err)
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
})
t.Run("should patch the config", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} })
oldConfig, _, err := client.GetConfig(context.Background())
require.NoError(t, err)
assert.False(t, *oldConfig.PasswordSettings.Lowercase)
assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength)
assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel)
assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
states := make(map[string]*model.PluginState)
states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewPointer(false)}
config := model.Config{PasswordSettings: model.PasswordSettings{
Lowercase: model.NewPointer(true),
MinimumLength: model.NewPointer(15),
}, LogSettings: model.LogSettings{
ConsoleLevel: model.NewPointer("INFO"),
},
TeamSettings: model.TeamSettings{
ExperimentalDefaultChannels: []string{"another-channel"},
},
PluginSettings: model.PluginSettings{
PluginStates: states,
},
}
_, response, err := client.PatchConfig(context.Background(), &config)
require.NoError(t, err)
updatedConfig, _, err := client.GetConfig(context.Background())
require.NoError(t, err)
assert.True(t, *updatedConfig.PasswordSettings.Lowercase)
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels)
assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control"))
// reset the config
_, _, err = client.UpdateConfig(context.Background(), oldConfig)
require.NoError(t, err)
})
t.Run("should sanitize config", func(t *testing.T) {
config := model.Config{PasswordSettings: model.PasswordSettings{
Symbol: model.NewPointer(true),
}}
updatedConfig, _, err := client.PatchConfig(context.Background(), &config)
require.NoError(t, err)
assert.Equal(t, model.FakeSetting, *updatedConfig.SqlSettings.DataSource)
})
t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) {
config := model.Config{PluginSettings: model.PluginSettings{
EnableUploads: model.NewPointer(true),
}}
updatedConfig, resp, err := client.PatchConfig(context.Background(), &config)
if client == th.LocalClient {
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, true, *updatedConfig.PluginSettings.EnableUploads)
} else {
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
})
t.Run("not allowing to change import directory via api, unless local mode", func(t *testing.T) {
oldDirectory := *th.App.Config().ImportSettings.Directory
config := model.Config{ImportSettings: model.ImportSettings{
Directory: model.NewPointer("./new-import-dir"),
}}
updatedConfig, resp, err := client.PatchConfig(context.Background(), &config)
if client == th.LocalClient {
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, "./new-import-dir", *updatedConfig.ImportSettings.Directory)
} else {
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
// Reset for local mode
if client == th.LocalClient {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ImportSettings.Directory = oldDirectory
})
}
})
})
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
oldURL := "hello.com"
newURL := "new.com"
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = false
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg := th.App.Config().Clone()
*cfg.PluginSettings.MarketplaceURL = newURL
_, _, err := th.SystemAdminClient.PatchConfig(context.Background(), cfg)
require.Error(t, err)
// Allowing uploads
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg = th.App.Config().Clone()
*cfg.PluginSettings.MarketplaceURL = newURL
cfg, _, err = th.SystemAdminClient.PatchConfig(context.Background(), cfg)
require.NoError(t, err)
assert.Equal(t, newURL, *cfg.PluginSettings.MarketplaceURL)
})
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
siteURL := cfg.ServiceSettings.SiteURL
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
// Set the SiteURL
nonEmptyURL := "http://localhost"
config := model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewPointer(nonEmptyURL),
},
}
updatedConfig, _, err := th.SystemAdminClient.PatchConfig(context.Background(), &config)
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *updatedConfig.ServiceSettings.SiteURL)
// Check that the Site URL can't be cleared
config = model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewPointer(""),
},
}
_, resp, err := th.SystemAdminClient.PatchConfig(context.Background(), &config)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
// Check that the Site URL wasn't cleared
cfg, _, err = th.SystemAdminClient.GetConfig(context.Background())
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
// Check that sending an empty config returns no error.
_, _, err = th.SystemAdminClient.PatchConfig(context.Background(), &model.Config{})
require.NoError(t, err)
})
}
func TestMigrateConfig(t *testing.T) {
th := Setup(t).InitBasic(t)
t.Run("LocalClient", func(t *testing.T) {
cfg := &model.Config{}
cfg.SetDefaults()
file, err := json.MarshalIndent(cfg, "", " ")
require.NoError(t, err)
err = os.WriteFile("from.json", file, 0644)
require.NoError(t, err)
defer os.Remove("from.json")
f, err := config.NewStoreFromDSN("from.json", false, nil, false)
require.NoError(t, err)
defer func() {
err = f.RemoveFile("from.json")
require.NoError(t, err)
}()
_, err = config.NewStoreFromDSN("to.json", false, nil, true)
require.NoError(t, err)
defer func() {
err = f.RemoveFile("to.json")
require.NoError(t, err)
}()
_, err = th.LocalClient.MigrateConfig(context.Background(), "from.json", "to.json")
require.NoError(t, err)
})
}