Files
mattermost/app/config.go
Jesús Espino 44079785eb Moving diagnostics into a service (#14832)
* Moving diagnostics into a service

* Fixing golint checks

* Fixing tests

* Renaming from diagnostics to telemetry

* Adding missing files

* Initializing telemetry earlier in the server startup

* Fixing tests

* Adding a log for the telemetryID initialization error

* Addressing PR review comments

* Fixing merge problem

* Removing some extra Diagnostics mentions

* Making tests pass
2020-09-08 20:30:54 +02:00

449 lines
13 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"runtime/debug"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v5/config"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/utils"
)
const (
ERROR_TERMS_OF_SERVICE_NO_ROWS_FOUND = "app.terms_of_service.get.no_rows.app_error"
)
func (s *Server) Config() *model.Config {
return s.configStore.Get()
}
func (a *App) Config() *model.Config {
return a.Srv().Config()
}
func (s *Server) EnvironmentConfig() map[string]interface{} {
return s.configStore.GetEnvironmentOverrides()
}
func (a *App) EnvironmentConfig() map[string]interface{} {
return a.Srv().EnvironmentConfig()
}
func (s *Server) UpdateConfig(f func(*model.Config)) {
old := s.Config()
updated := old.Clone()
f(updated)
if _, err := s.configStore.Set(updated); err != nil {
mlog.Error("Failed to update config", mlog.Err(err))
}
}
func (a *App) UpdateConfig(f func(*model.Config)) {
a.Srv().UpdateConfig(f)
}
func (s *Server) ReloadConfig() error {
debug.FreeOSMemory()
if err := s.configStore.Load(); err != nil {
return err
}
return nil
}
func (a *App) ReloadConfig() error {
return a.Srv().ReloadConfig()
}
func (a *App) ClientConfig() map[string]string {
return a.Srv().clientConfig.Load().(map[string]string)
}
func (a *App) ClientConfigHash() string {
return a.Srv().ClientConfigHash()
}
func (a *App) LimitedClientConfig() map[string]string {
return a.Srv().limitedClientConfig.Load().(map[string]string)
}
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
// for the listener that can later be used to remove it.
func (s *Server) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return s.configStore.AddListener(listener)
}
func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return a.Srv().AddConfigListener(listener)
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (s *Server) RemoveConfigListener(id string) {
s.configStore.RemoveListener(id)
}
func (a *App) RemoveConfigListener(id string) {
a.Srv().RemoveConfigListener(id)
}
// ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists
// and future calls to PostActionCookieSecret will always return a valid key, same on all
// servers in the cluster
func (s *Server) ensurePostActionCookieSecret() error {
if s.postActionCookieSecret != nil {
return nil
}
var secret *model.SystemPostActionCookieSecret
value, err := s.Store.System().GetByName(model.SYSTEM_POST_ACTION_COOKIE_SECRET)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if secret == nil {
newSecret := &model.SystemPostActionCookieSecret{
Secret: make([]byte, 32),
}
_, err := rand.Reader.Read(newSecret.Secret)
if err != nil {
return err
}
system := &model.System{
Name: model.SYSTEM_POST_ACTION_COOKIE_SECRET,
}
v, err := json.Marshal(newSecret)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = s.Store.System().Save(system); err != nil {
mlog.Error("Failed to save PostActionCookieSecret", mlog.Err(err))
} else {
secret = newSecret
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if secret == nil {
value, err := s.Store.System().GetByName(model.SYSTEM_POST_ACTION_COOKIE_SECRET)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
s.postActionCookieSecret = secret.Secret
return nil
}
// ensureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to
// AsymmetricSigningKey will always return a valid signing key.
func (s *Server) ensureAsymmetricSigningKey() error {
if s.asymmetricSigningKey != nil {
return nil
}
var key *model.SystemAsymmetricSigningKey
value, err := s.Store.System().GetByName(model.SYSTEM_ASYMMETRIC_SIGNING_KEY)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if key == nil {
newECDSAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
newKey := &model.SystemAsymmetricSigningKey{
ECDSAKey: &model.SystemECDSAKey{
Curve: "P-256",
X: newECDSAKey.X,
Y: newECDSAKey.Y,
D: newECDSAKey.D,
},
}
system := &model.System{
Name: model.SYSTEM_ASYMMETRIC_SIGNING_KEY,
}
v, err := json.Marshal(newKey)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = s.Store.System().Save(system); err != nil {
mlog.Error("Failed to save AsymmetricSigningKey", mlog.Err(err))
} else {
key = newKey
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if key == nil {
value, err := s.Store.System().GetByName(model.SYSTEM_ASYMMETRIC_SIGNING_KEY)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
var curve elliptic.Curve
switch key.ECDSAKey.Curve {
case "P-256":
curve = elliptic.P256()
default:
return fmt.Errorf("unknown curve: " + key.ECDSAKey.Curve)
}
s.asymmetricSigningKey = &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: key.ECDSAKey.X,
Y: key.ECDSAKey.Y,
},
D: key.ECDSAKey.D,
}
s.regenerateClientConfig()
return nil
}
func (s *Server) ensureInstallationDate() error {
_, appErr := s.getSystemInstallDate()
if appErr == nil {
return nil
}
installDate, appErr := s.Store.User().InferSystemInstallDate()
var installationDate int64
if appErr == nil && installDate > 0 {
installationDate = installDate
} else {
installationDate = utils.MillisFromTime(time.Now())
}
if err := s.Store.System().SaveOrUpdate(&model.System{
Name: model.SYSTEM_INSTALLATION_DATE_KEY,
Value: strconv.FormatInt(installationDate, 10),
}); err != nil {
return err
}
return nil
}
func (s *Server) ensureFirstServerRunTimestamp() error {
_, appErr := s.getFirstServerRunTimestamp()
if appErr == nil {
return nil
}
if err := s.Store.System().SaveOrUpdate(&model.System{
Name: model.SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY,
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
}); err != nil {
return err
}
return nil
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (s *Server) AsymmetricSigningKey() *ecdsa.PrivateKey {
return s.asymmetricSigningKey
}
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
return a.Srv().AsymmetricSigningKey()
}
func (s *Server) PostActionCookieSecret() []byte {
return s.postActionCookieSecret
}
func (a *App) PostActionCookieSecret() []byte {
return a.Srv().PostActionCookieSecret()
}
func (s *Server) regenerateClientConfig() {
clientConfig := config.GenerateClientConfig(s.Config(), s.TelemetryId(), s.License())
limitedClientConfig := config.GenerateLimitedClientConfig(s.Config(), s.TelemetryId(), s.License())
if clientConfig["EnableCustomTermsOfService"] == "true" {
termsOfService, err := s.Store.TermsOfService().GetLatest(true)
if err != nil {
mlog.Err(err)
} else {
clientConfig["CustomTermsOfServiceId"] = termsOfService.Id
limitedClientConfig["CustomTermsOfServiceId"] = termsOfService.Id
}
}
if key := s.AsymmetricSigningKey(); key != nil {
der, _ := x509.MarshalPKIXPublicKey(&key.PublicKey)
clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
limitedClientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
}
clientConfigJSON, _ := json.Marshal(clientConfig)
s.clientConfig.Store(clientConfig)
s.limitedClientConfig.Store(limitedClientConfig)
s.clientConfigHash.Store(fmt.Sprintf("%x", md5.Sum(clientConfigJSON)))
}
func (a *App) GetCookieDomain() string {
if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
return siteURL.Hostname()
}
}
return ""
}
func (a *App) GetSiteURL() string {
return *a.Config().ServiceSettings.SiteURL
}
// ClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (s *Server) ClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range s.clientConfig.Load().(map[string]string) {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(s.IsFirstUserAccount())
respCfg["MaxPostSize"] = strconv.Itoa(s.MaxPostSize())
respCfg["UpgradedFromTE"] = strconv.FormatBool(s.isUpgradedFromTE())
respCfg["InstallationDate"] = ""
if installationDate, err := s.getSystemInstallDate(); err == nil {
respCfg["InstallationDate"] = strconv.FormatInt(installationDate, 10)
}
return respCfg
}
// ClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (a *App) ClientConfigWithComputed() map[string]string {
return a.Srv().ClientConfigWithComputed()
}
// LimitedClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (a *App) LimitedClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range a.LimitedClientConfig() {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(a.IsFirstUserAccount())
return respCfg
}
// GetConfigFile proxies access to the given configuration file to the underlying config store.
func (a *App) GetConfigFile(name string) ([]byte, error) {
data, err := a.Srv().configStore.GetFile(name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get config file %s", name)
}
return data, nil
}
// GetSanitizedConfig gets the configuration for a system admin without any secrets.
func (a *App) GetSanitizedConfig() *model.Config {
cfg := a.Config().Clone()
cfg.Sanitize()
return cfg
}
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
func (a *App) GetEnvironmentConfig() map[string]interface{} {
return a.EnvironmentConfig()
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
func (s *Server) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError {
oldCfg, err := s.configStore.Set(newCfg)
if errors.Cause(err) == config.ErrReadOnlyConfiguration {
return model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, err.Error(), http.StatusForbidden)
} else if err != nil {
return model.NewAppError("saveConfig", "app.save_config.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if s.Metrics != nil {
if *s.Config().MetricsSettings.Enable {
s.Metrics.StartServer()
} else {
s.Metrics.StopServer()
}
}
if s.Cluster != nil {
newCfg = s.configStore.RemoveEnvironmentOverrides(newCfg)
oldCfg = s.configStore.RemoveEnvironmentOverrides(oldCfg)
err := s.Cluster.ConfigChanged(oldCfg, newCfg, sendConfigChangeClusterMessage)
if err != nil {
return err
}
}
return nil
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError {
return a.Srv().SaveConfig(newCfg, sendConfigChangeClusterMessage)
}
func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
// 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
// directly and not through the System Console UI.
if *cfg.MessageExportSettings.EnableExport != *appCfg.MessageExportSettings.EnableExport {
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.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis())
} else if !*cfg.MessageExportSettings.EnableExport {
// When the feature is disabled, reset the timestamp so that the timestamp will be set if
// the feature is re-enabled from the System Console in future.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(0)
}
}
}