Files
mattermost/utils/config.go
Jesse Hallam b98ef658ad MM-11720: disable loading plugin specific config from the environment (#9334)
There are numerous issues here, including some non-determinism in the viper library (fixable) and some annoying behaviour regarding periods in keys, often used by plugin ids (fix unknown). Let's defer the handling of same until we can get our config loading library to do what we need it to do vs. having to hack around viper all the time.
2018-09-03 17:08:25 +02:00

857 lines
32 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/mattermost/viper"
"github.com/pkg/errors"
"net/http"
"github.com/mattermost/mattermost-server/einterfaces"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils/jsonutils"
)
const (
LOG_ROTATE_SIZE = 10000
LOG_FILENAME = "mattermost.log"
)
var (
commonBaseSearchPaths = []string{
".",
"..",
"../..",
"../../..",
}
)
func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
if filepath.IsAbs(path) {
if _, err := os.Stat(path); err == nil {
return path
}
return ""
}
searchPaths := []string{}
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(searchPaths, baseSearchPath)
}
// Additionally attempt to search relative to the location of the running binary.
var binaryDir string
if exe, err := os.Executable(); err == nil {
if exe, err = filepath.EvalSymlinks(exe); err == nil {
if exe, err = filepath.Abs(exe); err == nil {
binaryDir = filepath.Dir(exe)
}
}
}
if binaryDir != "" {
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(
searchPaths,
filepath.Join(binaryDir, baseSearchPath),
)
}
}
for _, parent := range searchPaths {
found, err := filepath.Abs(filepath.Join(parent, path))
if err != nil {
continue
} else if fileInfo, err := os.Stat(found); err == nil {
if filter != nil {
if filter(fileInfo) {
return found
}
} else {
return found
}
}
}
return ""
}
// FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or
// relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty
// string is returned if no configuration is found.
func FindConfigFile(fileName string) (path string) {
found := FindFile(filepath.Join("config", fileName))
if found == "" {
found = FindPath(fileName, []string{"."}, nil)
}
return found
}
// FindFile looks for the given file in nearby ancestors relative to the current working
// directory as well as the directory of the executable.
func FindFile(path string) string {
return FindPath(path, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
// FindDir looks for the given directory in nearby ancestors relative to the current working
// directory as well as the directory of the executable, falling back to `./` if not found.
func FindDir(dir string) (string, bool) {
found := FindPath(dir, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
func MloggerConfigFromLoggerConfig(s *model.LogSettings) *mlog.LoggerConfiguration {
return &mlog.LoggerConfiguration{
EnableConsole: s.EnableConsole,
ConsoleJson: *s.ConsoleJson,
ConsoleLevel: strings.ToLower(s.ConsoleLevel),
EnableFile: s.EnableFile,
FileJson: *s.FileJson,
FileLevel: strings.ToLower(s.FileLevel),
FileLocation: GetLogFileLocation(s.FileLocation),
}
}
// DON'T USE THIS Modify the level on the app logger
func DisableDebugLogForTest() {
mlog.GloballyDisableDebugLogForTest()
}
// DON'T USE THIS Modify the level on the app logger
func EnableDebugLogForTest() {
mlog.GloballyEnableDebugLogForTest()
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = FindDir("logs")
}
return filepath.Join(fileLocation, LOG_FILENAME)
}
func SaveConfig(fileName string, config *model.Config) *model.AppError {
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusBadRequest)
}
err = ioutil.WriteFile(fileName, b, 0644)
if err != nil {
return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusInternalServerError)
}
return nil
}
type ConfigWatcher struct {
watcher *fsnotify.Watcher
close chan struct{}
closed chan struct{}
}
func NewConfigWatcher(cfgFileName string, f func()) (*ConfigWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, errors.Wrapf(err, "failed to create config watcher for file: "+cfgFileName)
}
configFile := filepath.Clean(cfgFileName)
configDir, _ := filepath.Split(configFile)
watcher.Add(configDir)
ret := &ConfigWatcher{
watcher: watcher,
close: make(chan struct{}),
closed: make(chan struct{}),
}
go func() {
defer close(ret.closed)
defer watcher.Close()
for {
select {
case event := <-watcher.Events:
// we only care about the config file
if filepath.Clean(event.Name) == configFile {
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
mlog.Info(fmt.Sprintf("Config file watcher detected a change reloading %v", cfgFileName))
if _, _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil {
f()
} else {
mlog.Error(fmt.Sprintf("Failed to read while watching config file at %v with err=%v", cfgFileName, configReadErr.Error()))
}
}
}
case err := <-watcher.Errors:
mlog.Error(fmt.Sprintf("Failed while watching config file at %v with err=%v", cfgFileName, err.Error()))
case <-ret.close:
return
}
}
}()
return ret, nil
}
func (w *ConfigWatcher) Close() {
close(w.close)
<-w.closed
}
// ReadConfig reads and parses the given configuration.
func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
// Pre-flight check the syntax of the configuration file to improve error messaging.
configData, err := ioutil.ReadAll(r)
if err != nil {
return nil, nil, err
} else {
var rawConfig interface{}
if err := json.Unmarshal(configData, &rawConfig); err != nil {
return nil, nil, jsonutils.HumanizeJsonError(err, configData)
}
}
v := newViper(allowEnvironmentOverrides)
if err := v.ReadConfig(bytes.NewReader(configData)); err != nil {
return nil, nil, err
}
var config model.Config
unmarshalErr := v.Unmarshal(&config)
// https://github.com/spf13/viper/issues/324
// https://github.com/spf13/viper/issues/348
if unmarshalErr == nil {
config.PluginSettings.Plugins = make(map[string]map[string]interface{})
unmarshalErr = v.UnmarshalKey("pluginsettings.plugins", &config.PluginSettings.Plugins)
}
if unmarshalErr == nil {
config.PluginSettings.PluginStates = make(map[string]*model.PluginState)
unmarshalErr = v.UnmarshalKey("pluginsettings.pluginstates", &config.PluginSettings.PluginStates)
}
envConfig := v.EnvSettings()
var envErr error
if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil {
return nil, nil, envErr
}
return &config, envConfig, unmarshalErr
}
func newViper(allowEnvironmentOverrides bool) *viper.Viper {
v := viper.New()
v.SetConfigType("json")
if allowEnvironmentOverrides {
v.SetEnvPrefix("mm")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
}
// Set zeroed defaults for all the config settings so that Viper knows what environment variables
// it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults.
defaults := getDefaultsFromStruct(model.Config{})
for key, value := range defaults {
if key == "PluginSettings.Plugins" || key == "PluginSettings.PluginStates" {
continue
}
v.SetDefault(key, value)
}
return v
}
func getDefaultsFromStruct(s interface{}) map[string]interface{} {
return flattenStructToMap(structToMap(reflect.TypeOf(s)))
}
// Converts a struct type into a nested map with keys matching the struct's fields and values
// matching the zeroed value of the corresponding field.
func structToMap(t reflect.Type) (out map[string]interface{}) {
defer func() {
if r := recover(); r != nil {
mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r))
}
}()
if t.Kind() != reflect.Struct {
// Should never hit this, but this will prevent a panic if that does happen somehow
return nil
}
out = map[string]interface{}{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
var value interface{}
switch field.Type.Kind() {
case reflect.Struct:
value = structToMap(field.Type)
case reflect.Ptr:
indirectType := field.Type.Elem()
if indirectType.Kind() == reflect.Struct {
// Follow pointers to structs since we need to define defaults for their fields
value = structToMap(indirectType)
} else {
value = nil
}
default:
value = reflect.Zero(field.Type).Interface()
}
out[field.Name] = value
}
return
}
// Flattens a nested map so that the result is a single map with keys corresponding to the
// path through the original map. For example,
// {
// "a": {
// "b": 1
// },
// "c": "sea"
// }
// would flatten to
// {
// "a.b": 1,
// "c": "sea"
// }
func flattenStructToMap(in map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{})
for key, value := range in {
if valueAsMap, ok := value.(map[string]interface{}); ok {
sub := flattenStructToMap(valueAsMap)
for subKey, subValue := range sub {
out[key+"."+subKey] = subValue
}
} else {
out[key] = value
}
}
return out
}
// Fixes the case of the environment variables sent back from Viper since Viper stores
// everything as lower case.
func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) {
defer func() {
if r := recover(); r != nil {
mlog.Error(fmt.Sprintf("Panicked in fixEnvSettingsCase. This should never happen. %v", r))
out = in
}
}()
var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{}
fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} {
if t.Kind() != reflect.Struct {
// Should never hit this, but this will prevent a panic if that does happen somehow
return nil
}
out := make(map[string]interface{}, len(in))
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
key := field.Name
if value, ok := in[strings.ToLower(key)]; ok {
if valueAsMap, ok := value.(map[string]interface{}); ok {
out[key] = fixCase(valueAsMap, field.Type)
} else {
out[key] = value
}
}
}
return out
}
out = fixCase(in, reflect.TypeOf(model.Config{}))
return
}
// ReadConfigFile reads and parses the configuration at the given file path.
func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, err
}
defer f.Close()
return ReadConfig(f, allowEnvironmentOverrides)
}
// EnsureConfigFile will attempt to locate a config file with the given name. If it does not exist,
// it will attempt to locate a default config file, and copy it to a file named fileName in the same
// directory. In either case, the config file path is returned.
func EnsureConfigFile(fileName string) (string, error) {
if configFile := FindConfigFile(fileName); configFile != "" {
return configFile, nil
}
if defaultPath := FindConfigFile("default.json"); defaultPath != "" {
destPath := filepath.Join(filepath.Dir(defaultPath), fileName)
src, err := os.Open(defaultPath)
if err != nil {
return "", err
}
defer src.Close()
dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
defer dest.Close()
if _, err := io.Copy(dest, src); err == nil {
return destPath, nil
}
}
return "", fmt.Errorf("no config file found")
}
// LoadConfig will try to search around for the corresponding config file. It will search
// /tmp/fileName then attempt ./config/fileName, then ../config/fileName and last it will look at
// fileName.
func LoadConfig(fileName string) (*model.Config, string, map[string]interface{}, *model.AppError) {
var configPath string
if fileName != filepath.Base(fileName) {
configPath = fileName
} else {
if path, err := EnsureConfigFile(fileName); err != nil {
appErr := model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
return nil, "", nil, appErr
} else {
configPath = path
}
}
config, envConfig, err := ReadConfigFile(configPath, true)
if err != nil {
appErr := model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
return nil, "", nil, appErr
}
needSave := len(config.SqlSettings.AtRestEncryptKey) == 0 || len(*config.FileSettings.PublicLinkSalt) == 0 ||
len(config.EmailSettings.InviteSalt) == 0
config.SetDefaults()
if err := config.IsValid(); err != nil {
return nil, "", nil, err
}
if needSave {
if err := SaveConfig(configPath, config); err != nil {
mlog.Warn(err.Error())
}
}
if err := ValidateLocales(config); err != nil {
if err := SaveConfig(configPath, config); err != nil {
mlog.Warn(err.Error())
}
}
if *config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
dir := config.FileSettings.Directory
if len(dir) > 0 && dir[len(dir)-1:] != "/" {
config.FileSettings.Directory += "/"
}
}
return config, configPath, envConfig, nil
}
func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
props := GenerateLimitedClientConfig(c, diagnosticId, license)
props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation)
props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
props["RestrictTeamInvite"] = *c.TeamSettings.RestrictTeamInvite
props["RestrictPublicChannelCreation"] = *c.TeamSettings.RestrictPublicChannelCreation
props["RestrictPrivateChannelCreation"] = *c.TeamSettings.RestrictPrivateChannelCreation
props["RestrictPublicChannelManagement"] = *c.TeamSettings.RestrictPublicChannelManagement
props["RestrictPrivateChannelManagement"] = *c.TeamSettings.RestrictPrivateChannelManagement
props["RestrictPublicChannelDeletion"] = *c.TeamSettings.RestrictPublicChannelDeletion
props["RestrictPrivateChannelDeletion"] = *c.TeamSettings.RestrictPrivateChannelDeletion
props["RestrictPrivateChannelManageMembers"] = *c.TeamSettings.RestrictPrivateChannelManageMembers
props["EnableXToLeaveChannelsFromLHS"] = strconv.FormatBool(*c.TeamSettings.EnableXToLeaveChannelsFromLHS)
props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks)
props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
props["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations)
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
props["RestrictPostDelete"] = *c.ServiceSettings.RestrictPostDelete
props["AllowEditPost"] = *c.ServiceSettings.AllowEditPost
props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
props["CloseUnusedDirectMessages"] = strconv.FormatBool(*c.ServiceSettings.CloseUnusedDirectMessages)
props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
if *c.ServiceSettings.ExperimentalChannelOrganization || *c.ServiceSettings.ExperimentalGroupUnreadChannels != model.GROUP_UNREAD_CHANNELS_DISABLED {
props["ExperimentalChannelOrganization"] = strconv.FormatBool(true)
} else {
props["ExperimentalChannelOrganization"] = strconv.FormatBool(false)
}
props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching)
props["EnablePreviewModeBanner"] = strconv.FormatBool(*c.EmailSettings.EnablePreviewModeBanner)
props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType
props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink)
props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
props["SQLDriverName"] = *c.SqlSettings.DriverName
props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
props["GfycatApiKey"] = *c.ServiceSettings.GfycatApiKey
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatApiSecret
props["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
props["EnableWebrtc"] = strconv.FormatBool(*c.WebrtcSettings.Enable)
props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
// Set default values for all options that require a license.
props["ExperimentalHideTownSquareinLHS"] = "false"
props["ExperimentalTownSquareIsReadOnly"] = "false"
props["ExperimentalEnableAuthenticationTransfer"] = "true"
props["LdapNicknameAttributeSet"] = "false"
props["LdapFirstNameAttributeSet"] = "false"
props["LdapLastNameAttributeSet"] = "false"
props["EnforceMultifactorAuthentication"] = "false"
props["EnableCompliance"] = "false"
props["EnableMobileFileDownload"] = "true"
props["EnableMobileFileUpload"] = "true"
props["SamlFirstNameAttributeSet"] = "false"
props["SamlLastNameAttributeSet"] = "false"
props["SamlNicknameAttributeSet"] = "false"
props["EnableCluster"] = "false"
props["EnableMetrics"] = "false"
props["PasswordMinimumLength"] = "0"
props["PasswordRequireLowercase"] = "false"
props["PasswordRequireUppercase"] = "false"
props["PasswordRequireNumber"] = "false"
props["PasswordRequireSymbol"] = "false"
props["EnableBanner"] = "false"
props["BannerText"] = ""
props["BannerColor"] = ""
props["BannerTextColor"] = ""
props["AllowBannerDismissal"] = "false"
props["EnableThemeSelection"] = "true"
props["DefaultTheme"] = ""
props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
props["CustomUrlSchemes"] = strings.Join(*c.DisplaySettings.CustomUrlSchemes, ",")
if license != nil {
props["ExperimentalHideTownSquareinLHS"] = strconv.FormatBool(*c.TeamSettings.ExperimentalHideTownSquareinLHS)
props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
if *license.Features.LDAP {
props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
}
if *license.Features.MFA {
props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
}
if *license.Features.Compliance {
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
}
if *license.Features.SAML {
props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
// do this under the correct licensed feature
props["ExperimentalClientSideCertEnable"] = strconv.FormatBool(*c.ExperimentalSettings.ClientSideCertEnable)
props["ExperimentalClientSideCertCheck"] = *c.ExperimentalSettings.ClientSideCertCheck
}
if *license.Features.Cluster {
props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
}
if *license.Features.Cluster {
props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
}
if *license.Features.Announcement {
props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
props["BannerText"] = *c.AnnouncementSettings.BannerText
props["BannerColor"] = *c.AnnouncementSettings.BannerColor
props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
}
if *license.Features.ThemeManagement {
props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
}
if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
}
}
return props
}
func GenerateLimitedClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
props := make(map[string]string)
props["Version"] = model.CurrentVersion
props["BuildNumber"] = model.BuildNumber
props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash
props["BuildHashEnterprise"] = model.BuildHashEnterprise
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
props["SiteName"] = c.TeamSettings.SiteName
props["EnableTeamCreation"] = strconv.FormatBool(*c.TeamSettings.EnableTeamCreation)
props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation)
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion
props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion
props["DesktopLatestVersion"] = c.ClientRequirements.DesktopLatestVersion
props["DesktopMinVersion"] = c.ClientRequirements.DesktopMinVersion
props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion
props["IosMinVersion"] = c.ClientRequirements.IosMinVersion
props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor
props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor
props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor
props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable)
props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
props["AboutLink"] = *c.SupportSettings.AboutLink
props["HelpLink"] = *c.SupportSettings.HelpLink
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
props["SupportEmail"] = *c.SupportSettings.SupportEmail
props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
props["DiagnosticId"] = diagnosticId
props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != ""
props["HasImageProxy"] = strconv.FormatBool(hasImageProxy)
// Set default values for all options that require a license.
props["EnableCustomBrand"] = "false"
props["CustomBrandText"] = ""
props["CustomDescriptionText"] = ""
props["EnableLdap"] = "false"
props["LdapLoginFieldName"] = ""
props["LdapLoginButtonColor"] = ""
props["LdapLoginButtonBorderColor"] = ""
props["LdapLoginButtonTextColor"] = ""
props["EnableMultifactorAuthentication"] = "false"
props["EnableSaml"] = "false"
props["SamlLoginButtonText"] = ""
props["SamlLoginButtonColor"] = ""
props["SamlLoginButtonBorderColor"] = ""
props["SamlLoginButtonTextColor"] = ""
props["EnableSignUpWithGoogle"] = "false"
props["EnableSignUpWithOffice365"] = "false"
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
if license != nil {
if *license.Features.LDAP {
props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
}
if *license.Features.MFA {
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
}
if *license.Features.SAML {
props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
}
if *license.Features.GoogleOAuth {
props["EnableSignUpWithGoogle"] = strconv.FormatBool(c.GoogleSettings.Enable)
}
if *license.Features.Office365OAuth {
props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable)
}
}
return props
}
func ValidateLdapFilter(cfg *model.Config, ldap einterfaces.LdapInterface) *model.AppError {
if *cfg.LdapSettings.Enable && ldap != nil && *cfg.LdapSettings.UserFilter != "" {
if err := ldap.ValidateFilter(*cfg.LdapSettings.UserFilter); err != nil {
return err
}
}
return nil
}
func ValidateLocales(cfg *model.Config) *model.AppError {
var err *model.AppError
locales := GetSupportedLocales()
if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
*cfg.LocalizationSettings.DefaultServerLocale = model.DEFAULT_LOCALE
err = model.NewAppError("ValidateLocales", "utils.config.supported_server_locale.app_error", nil, "", http.StatusBadRequest)
}
if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
*cfg.LocalizationSettings.DefaultClientLocale = model.DEFAULT_LOCALE
err = model.NewAppError("ValidateLocales", "utils.config.supported_client_locale.app_error", nil, "", http.StatusBadRequest)
}
if len(*cfg.LocalizationSettings.AvailableLocales) > 0 {
isDefaultClientLocaleInAvailableLocales := false
for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
if _, ok := locales[word]; !ok {
*cfg.LocalizationSettings.AvailableLocales = ""
isDefaultClientLocaleInAvailableLocales = true
err = model.NewAppError("ValidateLocales", "utils.config.supported_available_locales.app_error", nil, "", http.StatusBadRequest)
break
}
if word == *cfg.LocalizationSettings.DefaultClientLocale {
isDefaultClientLocaleInAvailableLocales = true
}
}
availableLocales := *cfg.LocalizationSettings.AvailableLocales
if !isDefaultClientLocaleInAvailableLocales {
availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
err = model.NewAppError("ValidateLocales", "utils.config.add_client_locale.app_error", nil, "", http.StatusBadRequest)
}
*cfg.LocalizationSettings.AvailableLocales = strings.Join(RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
}
return err
}