Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
8.9 KiB
Go
284 lines
8.9 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
"github.com/mattermost/mattermost/server/v8/platform/shared/mail"
|
|
)
|
|
|
|
const (
|
|
ErrorTermsOfServiceNoRowsFound = "app.terms_of_service.get.no_rows.app_error"
|
|
)
|
|
|
|
func (s *Server) Config() *model.Config {
|
|
return s.platform.Config()
|
|
}
|
|
|
|
func (a *App) Config() *model.Config {
|
|
return a.ch.cfgSvc.Config()
|
|
}
|
|
|
|
func (a *App) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
|
|
return a.Srv().platform.GetEnvironmentOverridesWithFilter(filter)
|
|
}
|
|
|
|
func (a *App) UpdateConfig(f func(*model.Config)) {
|
|
a.Srv().platform.UpdateConfig(f)
|
|
}
|
|
|
|
func (a *App) IsConfigReadOnly() bool {
|
|
return a.Srv().platform.IsConfigReadOnly()
|
|
}
|
|
|
|
func (a *App) ReloadConfig() error {
|
|
return a.Srv().platform.ReloadConfig()
|
|
}
|
|
|
|
func (a *App) ClientConfig() map[string]string {
|
|
return a.ch.srv.platform.ClientConfig()
|
|
}
|
|
|
|
func (a *App) ClientConfigHash() string {
|
|
return a.ch.ClientConfigHash()
|
|
}
|
|
|
|
func (a *App) LimitedClientConfig() map[string]string {
|
|
return a.ch.srv.platform.LimitedClientConfig()
|
|
}
|
|
|
|
func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
|
return a.Srv().platform.AddConfigListener(listener)
|
|
}
|
|
|
|
func (a *App) AddLicenseListener(listener func(oldLicense, newLicense *model.License)) string {
|
|
return a.Srv().platform.AddLicenseListener(listener)
|
|
}
|
|
|
|
// Removes a listener function by the unique ID returned when AddConfigListener was called
|
|
func (a *App) RemoveConfigListener(id string) {
|
|
a.Srv().platform.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 (ch *Channels) ensurePostActionCookieSecret() error {
|
|
if ch.postActionCookieSecret != nil {
|
|
return nil
|
|
}
|
|
|
|
var secret *model.SystemPostActionCookieSecret
|
|
|
|
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
|
|
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.SystemPostActionCookieSecretKey,
|
|
}
|
|
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 = ch.srv.Store().System().Save(system); err != nil {
|
|
mlog.Warn("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 := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ch.postActionCookieSecret = secret.Secret
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) ensureInstallationDate() error {
|
|
_, appErr := s.platform.GetSystemInstallDate()
|
|
if appErr == nil {
|
|
return nil
|
|
}
|
|
|
|
installDate, nErr := s.Store().User().InferSystemInstallDate()
|
|
var installationDate int64
|
|
if nErr == nil && installDate > 0 {
|
|
installationDate = installDate
|
|
} else {
|
|
installationDate = utils.MillisFromTime(time.Now())
|
|
}
|
|
|
|
if err := s.Store().System().SaveOrUpdate(&model.System{
|
|
Name: model.SystemInstallationDateKey,
|
|
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.SystemFirstServerRunTimestampKey,
|
|
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 (ch *Channels) AsymmetricSigningKey() *ecdsa.PrivateKey {
|
|
return ch.srv.platform.AsymmetricSigningKey()
|
|
}
|
|
|
|
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
|
|
return a.ch.AsymmetricSigningKey()
|
|
}
|
|
|
|
func (ch *Channels) PostActionCookieSecret() []byte {
|
|
return ch.postActionCookieSecret
|
|
}
|
|
|
|
func (a *App) PostActionCookieSecret() []byte {
|
|
return a.ch.PostActionCookieSecret()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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().platform.GetConfigFile(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()
|
|
|
|
a.SanitizedConfig(cfg)
|
|
|
|
return cfg
|
|
}
|
|
|
|
// SanitizedConfig sanitizes a given configuration for a system admin without any secrets.
|
|
func (a *App) SanitizedConfig(cfg *model.Config) {
|
|
manifests, err := a.getPluginManifests()
|
|
if err != nil {
|
|
// GetPluginManifests might error, e.g. when plugins are disabled.
|
|
// Sanitize all plugin settings in this case.
|
|
cfg.Sanitize(nil, nil)
|
|
return
|
|
}
|
|
|
|
cfg.Sanitize(manifests, nil)
|
|
}
|
|
|
|
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
|
|
// If filter is not nil and returns false for a struct field, that field will be omitted.
|
|
func (a *App) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
|
|
return a.EnvironmentConfig(filter)
|
|
}
|
|
|
|
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
|
|
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
|
|
return a.Srv().platform.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 && model.SafeDereference(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.NewPointer(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.NewPointer(int64(0))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) MailServiceConfig() *mail.SMTPConfig {
|
|
emailSettings := s.platform.Config().EmailSettings
|
|
hostname := utils.GetHostnameFromSiteURL(*s.platform.Config().ServiceSettings.SiteURL)
|
|
cfg := mail.SMTPConfig{
|
|
Hostname: hostname,
|
|
ConnectionSecurity: *emailSettings.ConnectionSecurity,
|
|
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
|
|
ServerName: *emailSettings.SMTPServer,
|
|
Server: *emailSettings.SMTPServer,
|
|
Port: *emailSettings.SMTPPort,
|
|
ServerTimeout: *emailSettings.SMTPServerTimeout,
|
|
Username: *emailSettings.SMTPUsername,
|
|
Password: *emailSettings.SMTPPassword,
|
|
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
|
|
SendEmailNotifications: *emailSettings.SendEmailNotifications,
|
|
FeedbackName: *emailSettings.FeedbackName,
|
|
FeedbackEmail: *emailSettings.FeedbackEmail,
|
|
ReplyToAddress: *emailSettings.ReplyToAddress,
|
|
}
|
|
return &cfg
|
|
}
|