MM-14441: restrict system admin config (#10477)

* tweak utils.Merge docs

* move merge_test to utils_test package for easier testing

* utils: support MergeConfig and StructFieldFilter

* constrain updating certain fields by the restricted system admin
Этот коммит содержится в:
Jesse Hallam
2019-03-21 15:46:38 -04:00
коммит произвёл GitHub
родитель 3d92af2737
Коммит 8c8b1bbc9c
10 изменённых файлов: 316 добавлений и 220 удалений

Просмотреть файл

@@ -5,8 +5,11 @@ package api4
import ( import (
"net/http" "net/http"
"reflect"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
) )
func (api *API) InitConfig() { func (api *API) InitConfig() {
@@ -58,13 +61,30 @@ func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
appCfg := c.App.Config()
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
// Start with the current configuration, and only merge values not marked as being
// restricted.
var err error
cfg, err = config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
restricted := structField.Tag.Get("restricted") == "true"
return !restricted
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// Do not allow plugin uploads to be toggled through the API // Do not allow plugin uploads to be toggled through the API
cfg.PluginSettings.EnableUploads = c.App.Config().PluginSettings.EnableUploads cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads
// If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an // If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an
// appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file // appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file
// directly and not through the System Console UI. // directly and not through the System Console UI.
if *cfg.MessageExportSettings.EnableExport != *c.App.Config().MessageExportSettings.EnableExport { if *cfg.MessageExportSettings.EnableExport != *appCfg.MessageExportSettings.EnableExport {
if *cfg.MessageExportSettings.EnableExport && *cfg.MessageExportSettings.ExportFromTimestamp == int64(0) { if *cfg.MessageExportSettings.EnableExport && *cfg.MessageExportSettings.ExportFromTimestamp == int64(0) {
// When the feature is toggled on, use the current timestamp as the start time for future exports. // When the feature is toggled on, use the current timestamp as the start time for future exports.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis()) cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis())

Просмотреть файл

@@ -196,6 +196,30 @@ func TestUpdateConfigMessageExportSpecialHandling(t *testing.T) {
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp) assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
} }
func TestUpdateConfigRestrictSystemAdmin(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
originalCfg, resp := th.SystemAdminClient.GetConfig()
CheckNoError(t, resp)
cfg := originalCfg.Clone()
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
returnedCfg, resp := th.SystemAdminClient.UpdateConfig(cfg)
CheckNoError(t, resp)
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL)
actualCfg, resp := th.SystemAdminClient.GetConfig()
CheckNoError(t, resp)
require.Equal(t, returnedCfg, actualCfg)
}
func TestGetEnvironmentConfig(t *testing.T) { func TestGetEnvironmentConfig(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com") os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true") os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")

Просмотреть файл

@@ -4,7 +4,6 @@
package config package config
import ( import (
"github.com/mattermost/mattermost-server/utils"
"io" "io"
"sync" "sync"
@@ -141,14 +140,3 @@ func (cs *commonStore) validate(cfg *model.Config) error {
return nil return nil
} }
// mergeConfig merges two configs together. The receiver's values are overwritten with the patch's
// values except when the patch's values are nil.
func (cs *commonStore) mergeConfig(patch *model.Config) (*model.Config, error) {
ret, err := utils.Merge(cs.config, patch)
if err != nil {
return nil, err
}
retC := ret.(model.Config)
return &retC, nil
}

Просмотреть файл

@@ -1,10 +1,11 @@
package config_test package config_test
import ( import (
"testing"
"github.com/mattermost/mattermost-server/config" "github.com/mattermost/mattermost-server/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"testing"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
@@ -85,7 +86,7 @@ func TestMergeConfigs(t *testing.T) {
patch, err := config.NewMemoryStore() patch, err := config.NewMemoryStore()
require.NoError(t, err) require.NoError(t, err)
merged, err := base.MergeConfig(patch.Get()) merged, err := config.Merge(base.Get(), patch.Get(), nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, patch.Get(), merged) assert.Equal(t, patch.Get(), merged)
@@ -95,7 +96,7 @@ func TestMergeConfigs(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
patch := base.Get().Clone() patch := base.Get().Clone()
merged, err := base.MergeConfig(patch) merged, err := config.Merge(base.Get(), patch, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, base.Get(), merged) assert.Equal(t, base.Get(), merged)
@@ -107,7 +108,7 @@ func TestMergeConfigs(t *testing.T) {
patch := base.Get().Clone() patch := base.Get().Clone()
patch.ServiceSettings.SiteURL = newString("http://newhost.ca") patch.ServiceSettings.SiteURL = newString("http://newhost.ca")
merged, err := base.MergeConfig(patch) merged, err := config.Merge(base.Get(), patch, nil)
require.NoError(t, err) require.NoError(t, err)
assert.NotEqual(t, base.Get(), merged) assert.NotEqual(t, base.Get(), merged)
@@ -124,7 +125,7 @@ func TestMergeConfigs(t *testing.T) {
expected.ServiceSettings.SiteURL = newString("http://newhost.ca") expected.ServiceSettings.SiteURL = newString("http://newhost.ca")
expected.GoogleSettings.Enable = newBool(true) expected.GoogleSettings.Enable = newBool(true)
merged, err := base.MergeConfig(patch) merged, err := config.Merge(base.Get(), patch, nil)
require.NoError(t, err) require.NoError(t, err)
assert.NotEqual(t, base.Get(), merged) assert.NotEqual(t, base.Get(), merged)

Просмотреть файл

@@ -26,8 +26,3 @@ func InitializeConfigurationsTable(db *sqlx.DB) error {
func ResolveConfigFilePath(path string) (string, error) { func ResolveConfigFilePath(path string) (string, error) {
return resolveConfigFilePath(path) return resolveConfigFilePath(path)
} }
// GetCommonStore exposes the internal commonStore to test only.
func (ms *memoryStore) MergeConfig(patch *model.Config) (*model.Config, error) {
return ms.mergeConfig(patch)
}

Просмотреть файл

@@ -128,3 +128,15 @@ func FixInvalidLocales(cfg *model.Config) bool {
return changed return changed
} }
// Merge merges two configs together. The receiver's values are overwritten with the patch's
// values except when the patch's values are nil.
func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) {
ret, err := utils.Merge(cfg, patch, mergeConfig)
if err != nil {
return nil, err
}
retCfg := ret.(model.Config)
return &retCfg, nil
}

Просмотреть файл

@@ -990,6 +990,10 @@
"id": "api.config.client.old_format.app_error", "id": "api.config.client.old_format.app_error",
"translation": "New format for the client configuration is not supported yet. Please specify format=old in the query string." "translation": "New format for the client configuration is not supported yet. Please specify format=old in the query string."
}, },
{
"id": "api.config.update_config.restricted_merge.app_error",
"translation": "Failed to merge given config."
},
{ {
"id": "api.context.404.app_error", "id": "api.context.404.app_error",
"translation": "Sorry, we could not find the page." "translation": "Sorry, we could not find the page."

Просмотреть файл

@@ -212,25 +212,25 @@ var ServerTLSSupportedCiphers = map[string]uint16{
} }
type ServiceSettings struct { type ServiceSettings struct {
SiteURL *string SiteURL *string `restricted:"true"`
WebsocketURL *string WebsocketURL *string `restricted:"true"`
LicenseFileLocation *string LicenseFileLocation *string `restricted:"true"`
ListenAddress *string ListenAddress *string `restricted:"true"`
ConnectionSecurity *string ConnectionSecurity *string `restricted:"true"`
TLSCertFile *string TLSCertFile *string `restricted:"true"`
TLSKeyFile *string TLSKeyFile *string `restricted:"true"`
TLSMinVer *string TLSMinVer *string `restricted:"true"`
TLSStrictTransport *bool TLSStrictTransport *bool `restricted:"true"`
TLSStrictTransportMaxAge *int64 TLSStrictTransportMaxAge *int64 `restricted:"true"`
TLSOverwriteCiphers []string TLSOverwriteCiphers []string `restricted:"true"`
UseLetsEncrypt *bool UseLetsEncrypt *bool `restricted:"true"`
LetsEncryptCertificateCacheFile *string LetsEncryptCertificateCacheFile *string `restricted:"true"`
Forward80To443 *bool Forward80To443 *bool `restricted:"true"`
ReadTimeout *int ReadTimeout *int `restricted:"true"`
WriteTimeout *int WriteTimeout *int `restricted:"true"`
MaximumLoginAttempts *int MaximumLoginAttempts *int `restricted:"true"`
GoroutineHealthThreshold *int GoroutineHealthThreshold *int `restricted:"true"`
GoogleDeveloperKey *string GoogleDeveloperKey *string `restricted:"true"`
EnableOAuthServiceProvider *bool EnableOAuthServiceProvider *bool
EnableIncomingWebhooks *bool EnableIncomingWebhooks *bool
EnableOutgoingWebhooks *bool EnableOutgoingWebhooks *bool
@@ -239,27 +239,27 @@ type ServiceSettings struct {
EnablePostUsernameOverride *bool EnablePostUsernameOverride *bool
EnablePostIconOverride *bool EnablePostIconOverride *bool
EnableLinkPreviews *bool EnableLinkPreviews *bool
EnableTesting *bool EnableTesting *bool `restricted:"true"`
EnableDeveloper *bool EnableDeveloper *bool `restricted:"true"`
EnableSecurityFixAlert *bool EnableSecurityFixAlert *bool `restricted:"true"`
EnableInsecureOutgoingConnections *bool EnableInsecureOutgoingConnections *bool `restricted:"true"`
AllowedUntrustedInternalConnections *string AllowedUntrustedInternalConnections *string `restricted:"true"`
EnableMultifactorAuthentication *bool EnableMultifactorAuthentication *bool
EnforceMultifactorAuthentication *bool EnforceMultifactorAuthentication *bool
EnableUserAccessTokens *bool EnableUserAccessTokens *bool
AllowCorsFrom *string AllowCorsFrom *string `restricted:"true"`
CorsExposedHeaders *string CorsExposedHeaders *string `restricted:"true"`
CorsAllowCredentials *bool CorsAllowCredentials *bool `restricted:"true"`
CorsDebug *bool CorsDebug *bool `restricted:"true"`
AllowCookiesForSubdomains *bool AllowCookiesForSubdomains *bool `restricted:"true"`
SessionLengthWebInDays *int SessionLengthWebInDays *int `restricted:"true"`
SessionLengthMobileInDays *int SessionLengthMobileInDays *int `restricted:"true"`
SessionLengthSSOInDays *int SessionLengthSSOInDays *int `restricted:"true"`
SessionCacheInMinutes *int SessionCacheInMinutes *int `restricted:"true"`
SessionIdleTimeoutInMinutes *int SessionIdleTimeoutInMinutes *int `restricted:"true"`
WebsocketSecurePort *int WebsocketSecurePort *int `restricted:"true"`
WebsocketPort *int WebsocketPort *int `restricted:"true"`
WebserverMode *string WebserverMode *string `restricted:"true"`
EnableCustomEmoji *bool EnableCustomEmoji *bool
EnableEmojiPicker *bool EnableEmojiPicker *bool
EnableGifPicker *bool EnableGifPicker *bool
@@ -269,14 +269,14 @@ type ServiceSettings struct {
DEPRECATED_DO_NOT_USE_RestrictPostDelete *string `json:"RestrictPostDelete"` // This field is deprecated and must not be used. DEPRECATED_DO_NOT_USE_RestrictPostDelete *string `json:"RestrictPostDelete"` // This field is deprecated and must not be used.
DEPRECATED_DO_NOT_USE_AllowEditPost *string `json:"AllowEditPost"` // This field is deprecated and must not be used. DEPRECATED_DO_NOT_USE_AllowEditPost *string `json:"AllowEditPost"` // This field is deprecated and must not be used.
PostEditTimeLimit *int PostEditTimeLimit *int
TimeBetweenUserTypingUpdatesMilliseconds *int64 TimeBetweenUserTypingUpdatesMilliseconds *int64 `restricted:"true"`
EnablePostSearch *bool EnablePostSearch *bool `restricted:"true"`
MinimumHashtagLength *int MinimumHashtagLength *int `restricted:"true"`
EnableUserTypingMessages *bool EnableUserTypingMessages *bool `restricted:"true"`
EnableChannelViewedMessages *bool EnableChannelViewedMessages *bool `restricted:"true"`
EnableUserStatuses *bool EnableUserStatuses *bool `restricted:"true"`
ExperimentalEnableAuthenticationTransfer *bool ExperimentalEnableAuthenticationTransfer *bool `restricted:"true"`
ClusterLogTimeoutMilliseconds *int ClusterLogTimeoutMilliseconds *int `restricted:"true"`
CloseUnusedDirectMessages *bool CloseUnusedDirectMessages *bool
EnablePreviewFeatures *bool EnablePreviewFeatures *bool
EnableTutorial *bool EnableTutorial *bool
@@ -288,11 +288,11 @@ type ServiceSettings struct {
DEPRECATED_DO_NOT_USE_ImageProxyOptions *string `json:"ImageProxyOptions" mapstructure:"ImageProxyOptions"` // This field is deprecated and must not be used. DEPRECATED_DO_NOT_USE_ImageProxyOptions *string `json:"ImageProxyOptions" mapstructure:"ImageProxyOptions"` // This field is deprecated and must not be used.
EnableAPITeamDeletion *bool EnableAPITeamDeletion *bool
ExperimentalEnableHardenedMode *bool ExperimentalEnableHardenedMode *bool
DisableLegacyMFA *bool DisableLegacyMFA *bool `restricted:"true"`
ExperimentalStrictCSRFEnforcement *bool ExperimentalStrictCSRFEnforcement *bool `restricted:"true"`
EnableEmailInvitations *bool EnableEmailInvitations *bool
ExperimentalLdapGroupSync *bool ExperimentalLdapGroupSync *bool
DisableBotsWhenOwnerIsDeactivated *bool DisableBotsWhenOwnerIsDeactivated *bool `restricted:"true"`
} }
func (s *ServiceSettings) SetDefaults() { func (s *ServiceSettings) SetDefaults() {
@@ -638,17 +638,17 @@ func (s *ServiceSettings) SetDefaults() {
} }
type ClusterSettings struct { type ClusterSettings struct {
Enable *bool Enable *bool `restricted:"true"`
ClusterName *string ClusterName *string `restricted:"true"`
OverrideHostname *string OverrideHostname *string `restricted:"true"`
UseIpAddress *bool UseIpAddress *bool `restricted:"true"`
UseExperimentalGossip *bool UseExperimentalGossip *bool `restricted:"true"`
ReadOnlyConfig *bool ReadOnlyConfig *bool `restricted:"true"`
GossipPort *int GossipPort *int `restricted:"true"`
StreamingPort *int StreamingPort *int `restricted:"true"`
MaxIdleConns *int MaxIdleConns *int `restricted:"true"`
MaxIdleConnsPerHost *int MaxIdleConnsPerHost *int `restricted:"true"`
IdleConnTimeoutMilliseconds *int IdleConnTimeoutMilliseconds *int `restricted:"true"`
} }
func (s *ClusterSettings) SetDefaults() { func (s *ClusterSettings) SetDefaults() {
@@ -698,9 +698,9 @@ func (s *ClusterSettings) SetDefaults() {
} }
type MetricsSettings struct { type MetricsSettings struct {
Enable *bool Enable *bool `restricted:"true"`
BlockProfileRate *int BlockProfileRate *int `restricted:"true"`
ListenAddress *string ListenAddress *string `restricted:"true"`
} }
func (s *MetricsSettings) SetDefaults() { func (s *MetricsSettings) SetDefaults() {
@@ -720,10 +720,10 @@ func (s *MetricsSettings) SetDefaults() {
type ExperimentalSettings struct { type ExperimentalSettings struct {
ClientSideCertEnable *bool ClientSideCertEnable *bool
ClientSideCertCheck *string ClientSideCertCheck *string
DisablePostMetadata *bool DisablePostMetadata *bool `restricted:"true"`
EnableClickToReply *bool EnableClickToReply *bool `restricted:"true"`
LinkMetadataTimeoutMilliseconds *int64 LinkMetadataTimeoutMilliseconds *int64 `restricted:"true"`
RestrictSystemAdmin *bool RestrictSystemAdmin *bool `restricted:"true"`
} }
func (s *ExperimentalSettings) SetDefaults() { func (s *ExperimentalSettings) SetDefaults() {
@@ -753,7 +753,7 @@ func (s *ExperimentalSettings) SetDefaults() {
} }
type AnalyticsSettings struct { type AnalyticsSettings struct {
MaxUsersForStatistics *int MaxUsersForStatistics *int `restricted:"true"`
} }
func (s *AnalyticsSettings) SetDefaults() { func (s *AnalyticsSettings) SetDefaults() {
@@ -803,16 +803,16 @@ func (s *SSOSettings) setDefaults() {
} }
type SqlSettings struct { type SqlSettings struct {
DriverName *string DriverName *string `restricted:"true"`
DataSource *string DataSource *string `restricted:"true"`
DataSourceReplicas []string DataSourceReplicas []string `restricted:"true"`
DataSourceSearchReplicas []string DataSourceSearchReplicas []string `restricted:"true"`
MaxIdleConns *int MaxIdleConns *int `restricted:"true"`
ConnMaxLifetimeMilliseconds *int ConnMaxLifetimeMilliseconds *int `restricted:"true"`
MaxOpenConns *int MaxOpenConns *int `restricted:"true"`
Trace *bool Trace *bool `restricted:"true"`
AtRestEncryptKey *string AtRestEncryptKey *string `restricted:"true"`
QueryTimeout *int QueryTimeout *int `restricted:"true"`
} }
func (s *SqlSettings) SetDefaults() { func (s *SqlSettings) SetDefaults() {
@@ -858,16 +858,16 @@ func (s *SqlSettings) SetDefaults() {
} }
type LogSettings struct { type LogSettings struct {
EnableConsole *bool EnableConsole *bool `restricted:"true"`
ConsoleLevel *string ConsoleLevel *string `restricted:"true"`
ConsoleJson *bool ConsoleJson *bool `restricted:"true"`
EnableFile *bool EnableFile *bool `restricted:"true"`
FileLevel *string FileLevel *string `restricted:"true"`
FileJson *bool FileJson *bool `restricted:"true"`
FileFormat *string FileFormat *string `restricted:"true"`
FileLocation *string FileLocation *string `restricted:"true"`
EnableWebhookDebugging *bool EnableWebhookDebugging *bool `restricted:"true"`
EnableDiagnostics *bool EnableDiagnostics *bool `restricted:"true"`
} }
func (s *LogSettings) SetDefaults() { func (s *LogSettings) SetDefaults() {
@@ -947,20 +947,20 @@ type FileSettings struct {
EnableMobileUpload *bool EnableMobileUpload *bool
EnableMobileDownload *bool EnableMobileDownload *bool
MaxFileSize *int64 MaxFileSize *int64
DriverName *string DriverName *string `restricted:"true"`
Directory *string Directory *string `restricted:"true"`
EnablePublicLink *bool EnablePublicLink *bool
PublicLinkSalt *string PublicLinkSalt *string
InitialFont *string InitialFont *string
AmazonS3AccessKeyId *string AmazonS3AccessKeyId *string `restricted:"true"`
AmazonS3SecretAccessKey *string AmazonS3SecretAccessKey *string `restricted:"true"`
AmazonS3Bucket *string AmazonS3Bucket *string `restricted:"true"`
AmazonS3Region *string AmazonS3Region *string `restricted:"true"`
AmazonS3Endpoint *string AmazonS3Endpoint *string `restricted:"true"`
AmazonS3SSL *bool AmazonS3SSL *bool `restricted:"true"`
AmazonS3SignV2 *bool AmazonS3SignV2 *bool `restricted:"true"`
AmazonS3SSE *bool AmazonS3SSE *bool `restricted:"true"`
AmazonS3Trace *bool AmazonS3Trace *bool `restricted:"true"`
} }
func (s *FileSettings) SetDefaults() { func (s *FileSettings) SetDefaults() {
@@ -1051,12 +1051,12 @@ type EmailSettings struct {
FeedbackEmail *string FeedbackEmail *string
ReplyToAddress *string ReplyToAddress *string
FeedbackOrganization *string FeedbackOrganization *string
EnableSMTPAuth *bool EnableSMTPAuth *bool `restricted:"true"`
SMTPUsername *string SMTPUsername *string `restricted:"true"`
SMTPPassword *string SMTPPassword *string `restricted:"true"`
SMTPServer *string SMTPServer *string `restricted:"true"`
SMTPPort *string SMTPPort *string `restricted:"true"`
ConnectionSecurity *string ConnectionSecurity *string `restricted:"true"`
SendPushNotifications *bool SendPushNotifications *bool
PushNotificationServer *string PushNotificationServer *string
PushNotificationContents *string PushNotificationContents *string
@@ -1064,7 +1064,7 @@ type EmailSettings struct {
EmailBatchingBufferSize *int EmailBatchingBufferSize *int
EmailBatchingInterval *int EmailBatchingInterval *int
EnablePreviewModeBanner *bool EnablePreviewModeBanner *bool
SkipServerCertificateVerification *bool SkipServerCertificateVerification *bool `restricted:"true"`
EmailNotificationContentsType *string EmailNotificationContentsType *string
LoginButtonColor *string LoginButtonColor *string
LoginButtonBorderColor *string LoginButtonBorderColor *string
@@ -1202,13 +1202,13 @@ func (s *EmailSettings) SetDefaults() {
} }
type RateLimitSettings struct { type RateLimitSettings struct {
Enable *bool Enable *bool `restricted:"true"`
PerSec *int PerSec *int `restricted:"true"`
MaxBurst *int MaxBurst *int `restricted:"true"`
MemoryStoreSize *int MemoryStoreSize *int `restricted:"true"`
VaryByRemoteAddr *bool VaryByRemoteAddr *bool `restricted:"true"`
VaryByUser *bool VaryByUser *bool `restricted:"true"`
VaryByHeader string VaryByHeader string `restricted:"true"`
} }
func (s *RateLimitSettings) SetDefaults() { func (s *RateLimitSettings) SetDefaults() {
@@ -1253,11 +1253,11 @@ func (s *PrivacySettings) setDefaults() {
} }
type SupportSettings struct { type SupportSettings struct {
TermsOfServiceLink *string TermsOfServiceLink *string `restricted:"true"`
PrivacyPolicyLink *string PrivacyPolicyLink *string `restricted:"true"`
AboutLink *string AboutLink *string `restricted:"true"`
HelpLink *string HelpLink *string `restricted:"true"`
ReportAProblemLink *string ReportAProblemLink *string `restricted:"true"`
SupportEmail *string SupportEmail *string
CustomTermsOfServiceEnabled *bool CustomTermsOfServiceEnabled *bool
CustomTermsOfServiceReAcceptancePeriod *int CustomTermsOfServiceReAcceptancePeriod *int
@@ -1551,12 +1551,12 @@ func (s *TeamSettings) SetDefaults() {
} }
type ClientRequirements struct { type ClientRequirements struct {
AndroidLatestVersion string AndroidLatestVersion string `restricted:"true"`
AndroidMinVersion string AndroidMinVersion string `restricted:"true"`
DesktopLatestVersion string DesktopLatestVersion string `restricted:"true"`
DesktopMinVersion string DesktopMinVersion string `restricted:"true"`
IosLatestVersion string IosLatestVersion string `restricted:"true"`
IosMinVersion string IosMinVersion string `restricted:"true"`
} }
type LdapSettings struct { type LdapSettings struct {
@@ -1901,9 +1901,9 @@ func (s *SamlSettings) SetDefaults() {
} }
type NativeAppSettings struct { type NativeAppSettings struct {
AppDownloadLink *string AppDownloadLink *string `restricted:"true"`
AndroidAppDownloadLink *string AndroidAppDownloadLink *string `restricted:"true"`
IosAppDownloadLink *string IosAppDownloadLink *string `restricted:"true"`
} }
func (s *NativeAppSettings) SetDefaults() { func (s *NativeAppSettings) SetDefaults() {
@@ -1921,25 +1921,25 @@ func (s *NativeAppSettings) SetDefaults() {
} }
type ElasticsearchSettings struct { type ElasticsearchSettings struct {
ConnectionUrl *string ConnectionUrl *string `restricted:"true"`
Username *string Username *string `restricted:"true"`
Password *string Password *string `restricted:"true"`
EnableIndexing *bool EnableIndexing *bool `restricted:"true"`
EnableSearching *bool EnableSearching *bool `restricted:"true"`
EnableAutocomplete *bool EnableAutocomplete *bool `restricted:"true"`
Sniff *bool Sniff *bool `restricted:"true"`
PostIndexReplicas *int PostIndexReplicas *int `restricted:"true"`
PostIndexShards *int PostIndexShards *int `restricted:"true"`
ChannelIndexReplicas *int ChannelIndexReplicas *int `restricted:"true"`
ChannelIndexShards *int ChannelIndexShards *int `restricted:"true"`
UserIndexReplicas *int UserIndexReplicas *int `restricted:"true"`
UserIndexShards *int UserIndexShards *int `restricted:"true"`
AggregatePostsAfterDays *int AggregatePostsAfterDays *int `restricted:"true"`
PostsAggregatorJobStartTime *string PostsAggregatorJobStartTime *string `restricted:"true"`
IndexPrefix *string IndexPrefix *string `restricted:"true"`
LiveIndexingBatchSize *int LiveIndexingBatchSize *int `restricted:"true"`
BulkIndexingTimeWindowSeconds *int BulkIndexingTimeWindowSeconds *int `restricted:"true"`
RequestTimeoutSeconds *int RequestTimeoutSeconds *int `restricted:"true"`
} }
func (s *ElasticsearchSettings) SetDefaults() { func (s *ElasticsearchSettings) SetDefaults() {
@@ -2051,8 +2051,8 @@ func (s *DataRetentionSettings) SetDefaults() {
} }
type JobSettings struct { type JobSettings struct {
RunJobs *bool RunJobs *bool `restricted:"true"`
RunScheduler *bool RunScheduler *bool `restricted:"true"`
} }
func (s *JobSettings) SetDefaults() { func (s *JobSettings) SetDefaults() {
@@ -2071,9 +2071,9 @@ type PluginState struct {
type PluginSettings struct { type PluginSettings struct {
Enable *bool Enable *bool
EnableUploads *bool EnableUploads *bool `restricted:"true"`
Directory *string Directory *string `restricted:"true"`
ClientDirectory *string ClientDirectory *string `restricted:"true"`
Plugins map[string]map[string]interface{} Plugins map[string]map[string]interface{}
PluginStates map[string]*PluginState PluginStates map[string]*PluginState
} }

Просмотреть файл

@@ -8,37 +8,40 @@ import (
"reflect" "reflect"
) )
// Merge will return a new struct/map/slice of the same type as base and patch, with patch merged into base. // StructFieldFilter defines a callback function used to decide if a patch value should be applied.
// Specifically, patch's values will be preferred except when patch's value is `nil`. type StructFieldFilter func(structField reflect.StructField, base reflect.Value, patch reflect.Value) bool
// Note: a referenced value (eg. *bool) will only be `nil` if the pointer is nil. If the value is a zero value,
// then that is considered a legitimate value. Eg, *bool(false) will overwrite *bool(true). // MergeConfig allows for optional merge customizations.
type MergeConfig struct {
StructFieldFilter StructFieldFilter
}
// Merge will return a new value of the same type as base and patch, recursively merging non-nil values from patch on top of base.
// //
// Restrictions/guarantees: // Restrictions/guarantees:
// - base and patch will not be modified
// - base and patch can be pointers or values
// - base and patch must be the same type // - base and patch must be the same type
// - if slices are different, this rule applies: // - base and patch will never be modified
// - if patch is not nil, overwrite the base slice. // - values from patch are always selected when non-nil
// - otherwise, keep the base slice // - structs are merged recursively
// - maps will be merged according to the following rules: // - maps and slices are treated as pointers, and merged as a single value
// - if patch is not nil, replace the base map completely
// - otherwise, keep the base map
// - reference values (eg. slice/ptr/map) will be cloned
// - channel values are not supported at the moment
// //
// Usage: callers need to cast the returned interface back into the original type, eg: // Note that callers need to cast the returned interface back into the original type:
// func mergeTestStruct(base, patch *testStruct) (*testStruct, error) { // func mergeTestStruct(base, patch *testStruct) (*testStruct, error) {
// ret, err := merge(base, patch) // ret, err := merge(base, patch)
// if err != nil { // if err != nil {
// return nil, err // return nil, err
// } // }
//
// retTS := ret.(testStruct) // retTS := ret.(testStruct)
// return &retTS, nil // return &retTS, nil
// } // }
func Merge(base interface{}, patch interface{}) (interface{}, error) { func Merge(base interface{}, patch interface{}, mergeConfig *MergeConfig) (interface{}, error) {
if reflect.TypeOf(base) != reflect.TypeOf(patch) { if reflect.TypeOf(base) != reflect.TypeOf(patch) {
return nil, fmt.Errorf("cannot merge different types. base type: %s, patch type: %s", return nil, fmt.Errorf(
reflect.TypeOf(base), reflect.TypeOf(patch)) "cannot merge different types. base type: %s, patch type: %s",
reflect.TypeOf(base),
reflect.TypeOf(patch),
)
} }
commonType := reflect.TypeOf(base) commonType := reflect.TypeOf(base)
@@ -52,7 +55,7 @@ func Merge(base interface{}, patch interface{}) (interface{}, error) {
ret := reflect.New(commonType) ret := reflect.New(commonType)
val, ok := merge(baseVal, patchVal) val, ok := merge(baseVal, patchVal, mergeConfig)
if ok { if ok {
ret.Elem().Set(val) ret.Elem().Set(val)
} }
@@ -60,7 +63,7 @@ func Merge(base interface{}, patch interface{}) (interface{}, error) {
} }
// merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value // merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value
func merge(base, patch reflect.Value) (reflect.Value, bool) { func merge(base, patch reflect.Value, mergeConfig *MergeConfig) (reflect.Value, bool) {
commonType := base.Type() commonType := base.Type()
switch commonType.Kind() { switch commonType.Kind() {
@@ -70,7 +73,13 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
if !merged.Field(i).CanSet() { if !merged.Field(i).CanSet() {
continue continue
} }
val, ok := merge(base.Field(i), patch.Field(i)) if mergeConfig != nil && mergeConfig.StructFieldFilter != nil {
if !mergeConfig.StructFieldFilter(commonType.Field(i), base.Field(i), patch.Field(i)) {
merged.Field(i).Set(base.Field(i))
continue
}
}
val, ok := merge(base.Field(i), patch.Field(i), mergeConfig)
if ok { if ok {
merged.Field(i).Set(val) merged.Field(i).Set(val)
} }
@@ -85,13 +94,13 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
// clone reference values (if any) // clone reference values (if any)
if base.IsNil() { if base.IsNil() {
val, _ := merge(patch.Elem(), patch.Elem()) val, _ := merge(patch.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val) mergedPtr.Elem().Set(val)
} else if patch.IsNil() { } else if patch.IsNil() {
val, _ := merge(base.Elem(), base.Elem()) val, _ := merge(base.Elem(), base.Elem(), mergeConfig)
mergedPtr.Elem().Set(val) mergedPtr.Elem().Set(val)
} else { } else {
val, _ := merge(base.Elem(), patch.Elem()) val, _ := merge(base.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val) mergedPtr.Elem().Set(val)
} }
return mergedPtr, true return mergedPtr, true
@@ -105,7 +114,7 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
merged := reflect.MakeSlice(commonType, 0, patch.Len()) merged := reflect.MakeSlice(commonType, 0, patch.Len())
for i := 0; i < patch.Len(); i++ { for i := 0; i < patch.Len(); i++ {
// recursively merge patch with itself. This will clone reference values. // recursively merge patch with itself. This will clone reference values.
val, _ := merge(patch.Index(i), patch.Index(i)) val, _ := merge(patch.Index(i), patch.Index(i), mergeConfig)
merged = reflect.Append(merged, val) merged = reflect.Append(merged, val)
} }
return merged, true return merged, true
@@ -115,7 +124,7 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
for i := 0; i < base.Len(); i++ { for i := 0; i < base.Len(); i++ {
// recursively merge base with itself. This will clone reference values. // recursively merge base with itself. This will clone reference values.
val, _ := merge(base.Index(i), base.Index(i)) val, _ := merge(base.Index(i), base.Index(i), mergeConfig)
merged = reflect.Append(merged, val) merged = reflect.Append(merged, val)
} }
return merged, true return merged, true
@@ -135,7 +144,7 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
} }
for _, key := range mapPtr.MapKeys() { for _, key := range mapPtr.MapKeys() {
// clone reference values // clone reference values
val, ok := merge(mapPtr.MapIndex(key), mapPtr.MapIndex(key)) val, ok := merge(mapPtr.MapIndex(key), mapPtr.MapIndex(key), mergeConfig)
if !ok { if !ok {
val = reflect.New(mapPtr.MapIndex(key).Type()).Elem() val = reflect.New(mapPtr.MapIndex(key).Type()).Elem()
} }
@@ -151,11 +160,11 @@ func merge(base, patch reflect.Value) (reflect.Value, bool) {
// clone reference values (if any) // clone reference values (if any)
if base.IsNil() { if base.IsNil() {
val, _ = merge(patch.Elem(), patch.Elem()) val, _ = merge(patch.Elem(), patch.Elem(), mergeConfig)
} else if patch.IsNil() { } else if patch.IsNil() {
val, _ = merge(base.Elem(), base.Elem()) val, _ = merge(base.Elem(), base.Elem(), mergeConfig)
} else { } else {
val, _ = merge(base.Elem(), patch.Elem()) val, _ = merge(base.Elem(), patch.Elem(), mergeConfig)
} }
return val, true return val, true

Просмотреть файл

@@ -1,11 +1,14 @@
package utils package utils_test
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/utils"
) )
// Test merging maps alone. This isolates the complexity of merging maps from merging maps recursively in // Test merging maps alone. This isolates the complexity of merging maps from merging maps recursively in
@@ -1156,6 +1159,38 @@ func TestMergeWithVeryComplexStruct(t *testing.T) {
}) })
} }
func TestMergeWithStructFieldFilter(t *testing.T) {
t.Run("filter skips merging from patch", func(t *testing.T) {
t1 := evenSimpler{newBool(true), &evenSimpler2{newString("base")}}
t2 := evenSimpler{newBool(false), &evenSimpler2{newString("patch")}}
expected := evenSimpler{newBool(true), &evenSimpler2{newString("base")}}
merged, err := mergeEvenSimplerWithConfig(t1, t2, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return false
},
})
require.NoError(t, err)
assert.Equal(t, expected, *merged)
})
t.Run("filter skips merging configured fields from patch", func(t *testing.T) {
t1 := evenSimpler{newBool(true), &evenSimpler2{newString("base")}}
t2 := evenSimpler{newBool(false), &evenSimpler2{newString("patch")}}
expected := evenSimpler{newBool(false), &evenSimpler2{newString("base")}}
merged, err := mergeEvenSimplerWithConfig(t1, t2, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return structField.Name == "B"
},
})
require.NoError(t, err)
assert.Equal(t, expected, *merged)
})
}
type testStruct struct { type testStruct struct {
I int I int
I8 int8 I8 int8
@@ -1504,11 +1539,10 @@ func setupStructs(t *testing.T) {
map[int]*string{1: newString("Another"), 2: newString("map of"), 3: newString("pointers, wow!")}, map[int]*string{1: newString("Another"), 2: newString("map of"), 3: newString("pointers, wow!")},
mergeStructEmbedBaseA, &mergeStructEmbedBaseB, mergeStructEmbedBaseA, &mergeStructEmbedBaseB,
} }
} }
func mergeSimple(base, patch simple) (*simple, error) { func mergeSimple(base, patch simple) (*simple, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1517,7 +1551,16 @@ func mergeSimple(base, patch simple) (*simple, error) {
} }
func mergeEvenSimpler(base, patch evenSimpler) (*evenSimpler, error) { func mergeEvenSimpler(base, patch evenSimpler) (*evenSimpler, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil {
return nil, err
}
retTS := ret.(evenSimpler)
return &retTS, nil
}
func mergeEvenSimplerWithConfig(base, patch evenSimpler, mergeConfig *utils.MergeConfig) (*evenSimpler, error) {
ret, err := utils.Merge(base, patch, mergeConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1526,7 +1569,7 @@ func mergeEvenSimpler(base, patch evenSimpler) (*evenSimpler, error) {
} }
func mergeSliceStruct(base, patch sliceStruct) (*sliceStruct, error) { func mergeSliceStruct(base, patch sliceStruct) (*sliceStruct, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1535,7 +1578,7 @@ func mergeSliceStruct(base, patch sliceStruct) (*sliceStruct, error) {
} }
func mergeMapPtr(base, patch mapPtr) (*mapPtr, error) { func mergeMapPtr(base, patch mapPtr) (*mapPtr, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1544,7 +1587,7 @@ func mergeMapPtr(base, patch mapPtr) (*mapPtr, error) {
} }
func mergeMapPtrState(base, patch mapPtrState) (*mapPtrState, error) { func mergeMapPtrState(base, patch mapPtrState) (*mapPtrState, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1553,7 +1596,7 @@ func mergeMapPtrState(base, patch mapPtrState) (*mapPtrState, error) {
} }
func mergeMapPtrState2(base, patch mapPtrState2) (*mapPtrState2, error) { func mergeMapPtrState2(base, patch mapPtrState2) (*mapPtrState2, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1562,7 +1605,7 @@ func mergeMapPtrState2(base, patch mapPtrState2) (*mapPtrState2, error) {
} }
func mergeTestStructs(base, patch testStruct) (*testStruct, error) { func mergeTestStructs(base, patch testStruct) (*testStruct, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1571,7 +1614,7 @@ func mergeTestStructs(base, patch testStruct) (*testStruct, error) {
} }
func mergeStringIntMap(base, patch map[string]int) (map[string]int, error) { func mergeStringIntMap(base, patch map[string]int) (map[string]int, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1580,7 +1623,7 @@ func mergeStringIntMap(base, patch map[string]int) (map[string]int, error) {
} }
func mergeStringPtrIntMap(base, patch map[string]*int) (map[string]*int, error) { func mergeStringPtrIntMap(base, patch map[string]*int) (map[string]*int, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1589,7 +1632,7 @@ func mergeStringPtrIntMap(base, patch map[string]*int) (map[string]*int, error)
} }
func mergeStringSliceIntMap(base, patch map[string][]int) (map[string][]int, error) { func mergeStringSliceIntMap(base, patch map[string][]int) (map[string][]int, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1598,7 +1641,7 @@ func mergeStringSliceIntMap(base, patch map[string][]int) (map[string][]int, err
} }
func mergeMapOfMap(base, patch map[string]map[string]*int) (map[string]map[string]*int, error) { func mergeMapOfMap(base, patch map[string]map[string]*int) (map[string]map[string]*int, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1607,7 +1650,7 @@ func mergeMapOfMap(base, patch map[string]map[string]*int) (map[string]map[strin
} }
func mergeInterfaceMap(base, patch map[string]interface{}) (map[string]interface{}, error) { func mergeInterfaceMap(base, patch map[string]interface{}) (map[string]interface{}, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1616,7 +1659,7 @@ func mergeInterfaceMap(base, patch map[string]interface{}) (map[string]interface
} }
func mergeStringSlices(base, patch []string) ([]string, error) { func mergeStringSlices(base, patch []string) ([]string, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1625,7 +1668,7 @@ func mergeStringSlices(base, patch []string) ([]string, error) {
} }
func mergeTestStructsPtrs(base, patch *testStruct) (*testStruct, error) { func mergeTestStructsPtrs(base, patch *testStruct) (*testStruct, error) {
ret, err := Merge(base, patch) ret, err := utils.Merge(base, patch, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }