From 463f7a05111824bc0ae2a6cc8bc2aabc95798296 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Mon, 2 Feb 2026 14:23:28 -0500 Subject: [PATCH] Cherry-pick MM-66789: Restrict log downloads to a root path for support packets (#35164) Automatic Merge --- server/channels/api4/apitestlib.go | 9 +- server/channels/api4/system.go | 9 + server/channels/app/helper_test.go | 8 +- server/channels/app/platform/config.go | 3 + server/channels/app/platform/log.go | 54 +- server/channels/app/platform/log_test.go | 188 ++++++- .../app/platform/support_packet_test.go | 3 + server/channels/app/support_packet_test.go | 3 + server/channels/testlib/helper.go | 14 + server/channels/testlib/resources.go | 6 + server/config/logger.go | 119 +++++ server/config/logger_test.go | 168 +++++++ server/public/model/audit_events.go | 473 ++++++++++++++++++ 13 files changed, 1050 insertions(+), 7 deletions(-) create mode 100644 server/public/model/audit_events.go diff --git a/server/channels/api4/apitestlib.go b/server/channels/api4/apitestlib.go index 1f95cef8a7..241b73f949 100644 --- a/server/channels/api4/apitestlib.go +++ b/server/channels/api4/apitestlib.go @@ -106,7 +106,14 @@ func setupTestHelper(tb testing.TB, dbStore store.Store, sqlSettings *model.SqlS consoleLevel = mlog.LvlStdLog.Name } *memoryConfig.LogSettings.ConsoleLevel = consoleLevel - *memoryConfig.LogSettings.FileLocation = filepath.Join(tempWorkspace, "logs", "mattermost.log") + // Use a subdirectory within the logging root (from MM_LOG_PATH or default) + // to ensure the path is within the allowed logging root for security validation. + // Each test gets its own subdirectory based on the tempWorkspace name for isolation. + testLogsDir := filepath.Join(config.GetLogRootPath(), filepath.Base(tempWorkspace)) + err = os.MkdirAll(testLogsDir, 0700) + require.NoError(tb, err, "failed to create test logs directory") + *memoryConfig.LogSettings.FileLocation = testLogsDir + *memoryConfig.NotificationLogSettings.FileLocation = testLogsDir *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false diff --git a/server/channels/api4/system.go b/server/channels/api4/system.go index feeb93f19e..921575e878 100644 --- a/server/channels/api4/system.go +++ b/server/channels/api4/system.go @@ -88,6 +88,9 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { return } + auditRec := c.MakeAuditRecord(model.AuditEventGenerateSupportPacket, model.AuditStatusFail) + defer c.LogAuditRec(auditRec) + // We support the existing API hence the logs are always included // if nothing specified. includeLogs := true @@ -99,6 +102,9 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { PluginPackets: r.Form["plugin_packets"], } + auditRec.AddMeta("include_logs", supportPacketOptions.IncludeLogs) + auditRec.AddMeta("plugin_packets", supportPacketOptions.PluginPackets) + // Checking to see if the server has a e10 or e20 license (this feature is only permitted for servers with licenses) if c.App.Channels().License() == nil { c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden) @@ -132,6 +138,9 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { } fileBytesReader := bytes.NewReader(fileBytes) + auditRec.Success() + auditRec.AddMeta("filename", outputZipFilename) + // Prevent caching so support packets are always fresh w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index 2599a298ef..ba30939a1d 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -78,7 +78,13 @@ func setupTestHelper(dbStore store.Store, sqlStore *sqlstore.SqlStore, sqlSettin *memoryConfig.LogSettings.ConsoleLevel = mlog.LvlStdLog.Name *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false - *memoryConfig.LogSettings.FileLocation = filepath.Join(tempWorkspace, "logs", "mattermost.log") + // Use a subdirectory within the logging root (from MM_LOG_PATH or default) + // to ensure the path is within the allowed logging root for security validation. + // Each test gets its own subdirectory based on the tempWorkspace name for isolation. + testLogsDir := filepath.Join(config.GetLogRootPath(), filepath.Base(tempWorkspace)) + err = os.MkdirAll(testLogsDir, 0700) + require.NoError(tb, err, "failed to create test logs directory") + *memoryConfig.LogSettings.FileLocation = testLogsDir if updateConfig != nil { updateConfig(memoryConfig) } diff --git a/server/channels/app/platform/config.go b/server/channels/app/platform/config.go index 95aff5250c..7af4d2b0e1 100644 --- a/server/channels/app/platform/config.go +++ b/server/channels/app/platform/config.go @@ -107,6 +107,9 @@ func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClus } } + // Validate log file paths (logs errors for now, will block server startup in future version) + config.WarnIfLogPathsOutsideRoot(newCfg) + oldCfg, newCfg, err := ps.configStore.Set(newCfg) if errors.Is(err, config.ErrReadOnlyConfiguration) { return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err) diff --git a/server/channels/app/platform/log.go b/server/channels/app/platform/log.go index d07ba5df4e..b2c9788a4a 100644 --- a/server/channels/app/platform/log.go +++ b/server/channels/app/platform/log.go @@ -215,12 +215,22 @@ func (ps *PlatformService) GetLogsSkipSend(rctx request.CTX, page, perPage int, return lines, nil } -func (ps *PlatformService) GetLogFile(_ request.CTX) (*model.FileData, error) { +func (ps *PlatformService) GetLogFile(rctx request.CTX) (*model.FileData, error) { if !*ps.Config().LogSettings.EnableFile { return nil, errors.New("Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false") } mattermostLog := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation) + + // Validate the file path to prevent arbitrary file reads + if err := ps.validateLogFilePath(mattermostLog); err != nil { + rctx.Logger().Error("Blocked attempt to read log file outside allowed root", + mlog.String("path", mattermostLog), + mlog.String("config_section", "LogSettings.FileLocation"), + mlog.Err(err)) + return nil, errors.Wrapf(err, "log file path %s is outside allowed logging directory", mattermostLog) + } + mattermostLogFileData, err := os.ReadFile(mattermostLog) if err != nil { return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog) @@ -232,12 +242,22 @@ func (ps *PlatformService) GetLogFile(_ request.CTX) (*model.FileData, error) { }, nil } -func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileData, error) { +func (ps *PlatformService) GetNotificationLogFile(rctx request.CTX) (*model.FileData, error) { if !*ps.Config().NotificationLogSettings.EnableFile { return nil, errors.New("Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false") } notificationsLog := config.GetNotificationsLogFileLocation(*ps.Config().NotificationLogSettings.FileLocation) + + // Validate the file path to prevent arbitrary file reads + if err := ps.validateLogFilePath(notificationsLog); err != nil { + rctx.Logger().Error("Blocked attempt to read log file outside allowed root", + mlog.String("path", notificationsLog), + mlog.String("config_section", "NotificationLogSettings.FileLocation"), + mlog.Err(err)) + return nil, errors.Wrapf(err, "log file path %s is outside allowed logging directory", notificationsLog) + } + notificationsLogFileData, err := os.ReadFile(notificationsLog) if err != nil { return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog) @@ -249,12 +269,26 @@ func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileDat }, nil } -func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, error) { +// validateLogFilePath validates that a log file path is within the logging root directory. +// This prevents arbitrary file read/write vulnerabilities in logging configuration. +// The logging root is determined by MM_LOG_PATH environment variable or the default logs directory. +// Currently used to validate paths when reading logs via GetAdvancedLogs. +// In future versions, this will also be used to validate paths when saving logging config. +func (ps *PlatformService) validateLogFilePath(filePath string) error { + // Get the logging root path (from env var or default logs directory) + loggingRoot := config.GetLogRootPath() + + return config.ValidateLogFilePath(filePath, loggingRoot) +} + +func (ps *PlatformService) GetAdvancedLogs(rctx request.CTX) ([]*model.FileData, error) { var ( rErr *multierror.Error ret []*model.FileData ) + rctx.Logger().Debug("Advanced logs access requested") + for name, loggingJSON := range map[string]json.RawMessage{ "LogSettings.AdvancedLoggingJSON": ps.Config().LogSettings.AdvancedLoggingJSON, "NotificationLogSettings.AdvancedLoggingJSON": ps.Config().NotificationLogSettings.AdvancedLoggingJSON, @@ -282,6 +316,18 @@ func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, er rErr = multierror.Append(rErr, errors.Wrapf(err, "error decoding file target options in %s", name)) continue } + + // Validate the file path to prevent arbitrary file reads + if err := ps.validateLogFilePath(fileOption.Filename); err != nil { + rctx.Logger().Error("Blocked attempt to read log file outside allowed root", + mlog.String("path", fileOption.Filename), + mlog.String("config_section", name), + mlog.String("user_id", rctx.Session().UserId), + mlog.Err(err)) + rErr = multierror.Append(rErr, errors.Wrapf(err, "log file path %s in %s is outside allowed logging directory", fileOption.Filename, name)) + continue + } + data, err := os.ReadFile(fileOption.Filename) if err != nil { rErr = multierror.Append(rErr, errors.Wrapf(err, "failed to read advanced log file at path %s in %s", fileOption.Filename, name)) @@ -296,7 +342,7 @@ func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, er } } - return ret, nil + return ret, rErr.ErrorOrNil() } func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool { diff --git a/server/channels/app/platform/log_test.go b/server/channels/app/platform/log_test.go index 8f300e5d89..771d63c368 100644 --- a/server/channels/app/platform/log_test.go +++ b/server/channels/app/platform/log_test.go @@ -48,6 +48,9 @@ func TestGetMattermostLog(t *testing.T) { assert.NoError(t, err) }) + // Set MM_LOG_PATH to allow log file reads from our temp directory + t.Setenv("MM_LOG_PATH", dir) + // Enable log file but point to an empty directory to get an error trying to read the file th.Service.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.EnableFile = true @@ -71,6 +74,33 @@ func TestGetMattermostLog(t *testing.T) { require.NotNil(t, fileData) assert.Equal(t, "mattermost.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) + + // Test path validation: FileLocation outside MM_LOG_PATH should be blocked + t.Run("path validation prevents reading files outside log directory", func(t *testing.T) { + // Create a directory outside the allowed log root + outsideDir, err := os.MkdirTemp("", "outside") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(outsideDir) + require.NoError(t, err) + }) + + // Create a file that would be read if validation fails + outsideLogLocation := config.GetLogFileLocation(outsideDir) + err = os.WriteFile(outsideLogLocation, []byte("secret data"), 0644) + require.NoError(t, err) + + // Set FileLocation to the outside directory (MM_LOG_PATH is still set to 'dir') + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.LogSettings.FileLocation = outsideDir + }) + + // Should be blocked by path validation + fileData, err = th.Service.GetLogFile(th.Context) + assert.Nil(t, fileData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside allowed logging directory") + }) } func TestGetNotificationLogFile(t *testing.T) { @@ -91,10 +121,20 @@ func TestGetNotificationLogFile(t *testing.T) { dir, err := os.MkdirTemp("", "") require.NoError(t, err) t.Cleanup(func() { + // Disable file target before cleaning up to avoid a race between + // removing the directory and the file getting written again. + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.NotificationLogSettings.EnableFile = false + }) + th.Service.NotificationsLogger().Flush() + err = os.RemoveAll(dir) assert.NoError(t, err) }) + // Set MM_LOG_PATH to allow log file reads from our temp directory + t.Setenv("MM_LOG_PATH", dir) + // Enable notifications file but point to an empty directory to get an error trying to read the file th.Service.UpdateConfig(func(cfg *model.Config) { *cfg.NotificationLogSettings.EnableFile = true @@ -118,6 +158,33 @@ func TestGetNotificationLogFile(t *testing.T) { require.NotNil(t, fileData) assert.Equal(t, "notifications.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) + + // Test path validation: FileLocation outside MM_LOG_PATH should be blocked + t.Run("path validation prevents reading files outside log directory", func(t *testing.T) { + // Create a directory outside the allowed log root + outsideDir, err := os.MkdirTemp("", "outside") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(outsideDir) + require.NoError(t, err) + }) + + // Create a file that would be read if validation fails + outsideLogLocation := config.GetNotificationsLogFileLocation(outsideDir) + err = os.WriteFile(outsideLogLocation, []byte("secret data"), 0644) + require.NoError(t, err) + + // Set FileLocation to the outside directory (MM_LOG_PATH is still set to 'dir') + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.NotificationLogSettings.FileLocation = outsideDir + }) + + // Should be blocked by path validation + fileData, err = th.Service.GetNotificationLogFile(th.Context) + assert.Nil(t, fileData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside allowed logging directory") + }) } func TestGetAdvancedLogs(t *testing.T) { @@ -134,6 +201,9 @@ func TestGetAdvancedLogs(t *testing.T) { require.NoError(t, err) }) + // Set MM_LOG_PATH to allow advanced logging to write to our temp directory + t.Setenv("MM_LOG_PATH", dir) + // Setup log files for each setting optLDAP := map[string]string{ "filename": path.Join(dir, "ldap.log"), @@ -243,9 +313,10 @@ func TestGetAdvancedLogs(t *testing.T) { require.NotNil(t, notifFile) testlib.AssertLog(t, bytes.NewBuffer(notifFile.Body), mlog.LvlInfo.Name, "Some Notification") }) - // Disable AdvancedLoggingJSON + // Disable AdvancedLoggingJSON for all log settings th.Service.UpdateConfig(func(c *model.Config) { c.LogSettings.AdvancedLoggingJSON = nil + c.NotificationLogSettings.AdvancedLoggingJSON = nil }) t.Run("No logs returned when AdvancedLoggingJSON is empty", func(t *testing.T) { // Confirm no logs get returned @@ -253,4 +324,119 @@ func TestGetAdvancedLogs(t *testing.T) { require.NoError(t, err) require.Len(t, fileDatas, 0) }) + + t.Run("path validation prevents reading files outside log directory", func(t *testing.T) { + // Create a temporary directory to use as the log root + logDir, err := os.MkdirTemp("", "logs") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(logDir) + require.NoError(t, err) + }) + + // Set MM_LOG_PATH to restrict log file access to logDir + t.Setenv("MM_LOG_PATH", logDir) + + // Create a file outside the log directory that should not be accessible + outsideDir, err := os.MkdirTemp("", "outside") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(outsideDir) + require.NoError(t, err) + }) + + secretFile := path.Join(outsideDir, "secret.txt") + err = os.WriteFile(secretFile, []byte("secret data"), 0644) + require.NoError(t, err) + + // Create a valid log file inside the log directory + validLog := path.Join(logDir, "valid.log") + err = os.WriteFile(validLog, []byte("valid log data"), 0644) + require.NoError(t, err) + + // Test 1: Attempt to read file outside log directory using absolute path + optOutside := map[string]string{ + "filename": secretFile, + } + dataOutside, err := json.Marshal(optOutside) + require.NoError(t, err) + + logCfgOutside := mlog.LoggerConfiguration{ + "malicious": mlog.TargetCfg{ + Type: "file", + Format: "json", + Levels: []mlog.Level{mlog.LvlError}, + Options: dataOutside, + }, + } + logCfgDataOutside, err := json.Marshal(logCfgOutside) + require.NoError(t, err) + + th.Service.UpdateConfig(func(c *model.Config) { + c.LogSettings.AdvancedLoggingJSON = logCfgDataOutside + }) + + fileDatas, err := th.Service.GetAdvancedLogs(th.Context) + // Should return error indicating path is outside allowed directory + require.Error(t, err) + require.Contains(t, err.Error(), "outside allowed logging directory") + require.Len(t, fileDatas, 0) + + // Test 2: Attempt path traversal attack + traversalPath := path.Join(logDir, "..", "..", "etc", "passwd") + optTraversal := map[string]string{ + "filename": traversalPath, + } + dataTraversal, err := json.Marshal(optTraversal) + require.NoError(t, err) + + logCfgTraversal := mlog.LoggerConfiguration{ + "traversal": mlog.TargetCfg{ + Type: "file", + Format: "json", + Levels: []mlog.Level{mlog.LvlError}, + Options: dataTraversal, + }, + } + logCfgDataTraversal, err := json.Marshal(logCfgTraversal) + require.NoError(t, err) + + th.Service.UpdateConfig(func(c *model.Config) { + c.LogSettings.AdvancedLoggingJSON = logCfgDataTraversal + }) + + fileDatas, err = th.Service.GetAdvancedLogs(th.Context) + // Should return error for path traversal attempt + require.Error(t, err) + require.Contains(t, err.Error(), "outside") + require.Len(t, fileDatas, 0) + + // Test 3: Valid path within log directory should work + optValid := map[string]string{ + "filename": validLog, + } + dataValid, err := json.Marshal(optValid) + require.NoError(t, err) + + logCfgValid := mlog.LoggerConfiguration{ + "valid": mlog.TargetCfg{ + Type: "file", + Format: "json", + Levels: []mlog.Level{mlog.LvlError}, + Options: dataValid, + }, + } + logCfgDataValid, err := json.Marshal(logCfgValid) + require.NoError(t, err) + + th.Service.UpdateConfig(func(c *model.Config) { + c.LogSettings.AdvancedLoggingJSON = logCfgDataValid + }) + + fileDatas, err = th.Service.GetAdvancedLogs(th.Context) + require.NoError(t, err) + require.Len(t, fileDatas, 1) + require.Equal(t, "valid.log", fileDatas[0].Filename) + require.Equal(t, []byte("valid log data"), fileDatas[0].Body) + }) } diff --git a/server/channels/app/platform/support_packet_test.go b/server/channels/app/platform/support_packet_test.go index f6c30989ce..693ca64f01 100644 --- a/server/channels/app/platform/support_packet_test.go +++ b/server/channels/app/platform/support_packet_test.go @@ -37,6 +37,9 @@ func TestGenerateSupportPacket(t *testing.T) { assert.NoError(t, err) }) + // Set MM_LOG_PATH to allow log file reads from our temp directory + t.Setenv("MM_LOG_PATH", dir) + th.Service.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.FileLocation = dir *cfg.NotificationLogSettings.FileLocation = dir diff --git a/server/channels/app/support_packet_test.go b/server/channels/app/support_packet_test.go index 098a511320..92c0314524 100644 --- a/server/channels/app/support_packet_test.go +++ b/server/channels/app/support_packet_test.go @@ -36,6 +36,9 @@ func TestGenerateSupportPacket(t *testing.T) { assert.NoError(t, err) }) + // Set MM_LOG_PATH to allow log file reads from our temp directory + t.Setenv("MM_LOG_PATH", dir) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.FileLocation = dir *cfg.NotificationLogSettings.FileLocation = dir diff --git a/server/channels/testlib/helper.go b/server/channels/testlib/helper.go index 1ef584c7e3..00051cf34c 100644 --- a/server/channels/testlib/helper.go +++ b/server/channels/testlib/helper.go @@ -35,6 +35,7 @@ type MainHelper struct { status int testResourcePath string + testLogsPath string replicas []string storePool *sqlstore.TestPool } @@ -88,6 +89,15 @@ func NewMainHelperWithOptions(options *HelperOptions) *MainHelper { log.Fatal(err) } + // Create a logs directory and set MM_LOG_PATH for tests that validate log file paths. + // This is done unconditionally so tests don't need to enable full resources just for logging. + logsDir, err := os.MkdirTemp("", "testlogs") + if err != nil { + log.Fatal("Failed to create test logs directory: " + err.Error()) + } + os.Setenv("MM_LOG_PATH", logsDir) + mainHelper.testLogsPath = logsDir + if options != nil { mainHelper.Options = *options @@ -293,6 +303,10 @@ func (h *MainHelper) Close() error { if h.testResourcePath != "" { os.RemoveAll(h.testResourcePath) } + if h.testLogsPath != "" { + os.RemoveAll(h.testLogsPath) + os.Unsetenv("MM_LOG_PATH") + } if h.storePool != nil { h.storePool.Close() diff --git a/server/channels/testlib/resources.go b/server/channels/testlib/resources.go index fd3640510b..dadeb48beb 100644 --- a/server/channels/testlib/resources.go +++ b/server/channels/testlib/resources.go @@ -142,6 +142,12 @@ func SetupTestResources() (string, error) { return "", errors.Wrapf(err, "failed to create client directory %s", clientDir) } + logsDir := path.Join(tempDir, "logs") + err = os.Mkdir(logsDir, 0700) + if err != nil { + return "", errors.Wrapf(err, "failed to create logs directory %s", logsDir) + } + err = setupConfig(path.Join(tempDir, "config")) if err != nil { return "", errors.Wrap(err, "failed to setup config") diff --git a/server/config/logger.go b/server/config/logger.go index dce3ca6f6e..3fa40e68f3 100644 --- a/server/config/logger.go +++ b/server/config/logger.go @@ -6,11 +6,13 @@ package config import ( "encoding/json" "fmt" + "os" "path/filepath" "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) @@ -127,6 +129,123 @@ func GetLogSettingsFromNotificationsLogSettings(notificationLogSettings *model.N return settings } +// GetLogRootPath returns the root directory for all log files. +// This is used for security validation to prevent arbitrary file reads via advanced logging. +// The logging root is determined by: +// 1. MM_LOG_PATH environment variable (if set and non-empty) +// 2. The default "logs" directory (found relative to the binary) +func GetLogRootPath() string { + // Check environment variable first + if envPath := os.Getenv("MM_LOG_PATH"); envPath != "" { + absPath, err := filepath.Abs(envPath) + if err == nil { + return absPath + } + } + + // Fall back to default logs directory + logsDir, _ := fileutils.FindDir("logs") + absPath, err := filepath.Abs(logsDir) + if err != nil { + return logsDir + } + return absPath +} + +// ValidateLogFilePath validates that a log file path is within the logging root directory. +// This prevents arbitrary file read/write vulnerabilities in logging configuration. +// The logging root is determined by MM_LOG_PATH environment variable or the configured log directory. +func ValidateLogFilePath(filePath string, loggingRoot string) error { + // Resolve file path to absolute + absPath, err := filepath.Abs(filePath) + if err != nil { + return fmt.Errorf("cannot resolve path %s: %w", filePath, err) + } + + // Resolve symlinks to prevent bypass via symlink attacks + realPath, err := filepath.EvalSymlinks(absPath) + if err != nil { + // If file doesn't exist, still validate the intended path + if !os.IsNotExist(err) { + return fmt.Errorf("cannot resolve symlinks for %s: %w", absPath, err) + } + } else { + absPath = realPath + } + + // Resolve logging root to absolute + absRoot, err := filepath.Abs(loggingRoot) + if err != nil { + return fmt.Errorf("cannot resolve logging root %s: %w", loggingRoot, err) + } + + // Ensure root has trailing separator for proper prefix matching + // This prevents /tmp/log matching /tmp/logger + rootWithSep := absRoot + if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) { + rootWithSep += string(filepath.Separator) + } + + // Check if file is within the logging root + // Allow exact match (absPath == absRoot) or proper prefix match + if absPath != absRoot && !strings.HasPrefix(absPath, rootWithSep) { + return fmt.Errorf("path %s is outside logging root %s", filePath, absRoot) + } + + return nil +} + +// WarnIfLogPathsOutsideRoot validates log file paths in the config and logs errors for paths outside the logging root. +// This is called during config save to identify configurations that will cause server startup to fail in a future version. +// Currently only logs errors; in a future version this will block server startup. +func WarnIfLogPathsOutsideRoot(cfg *model.Config) { + loggingRoot := GetLogRootPath() + + // Check LogSettings.AdvancedLoggingJSON + if !utils.IsEmptyJSON(cfg.LogSettings.AdvancedLoggingJSON) { + validateAdvancedLoggingConfig(cfg.LogSettings.AdvancedLoggingJSON, "LogSettings.AdvancedLoggingJSON", loggingRoot) + } + + // Check NotificationLogSettings.AdvancedLoggingJSON + if !utils.IsEmptyJSON(cfg.NotificationLogSettings.AdvancedLoggingJSON) { + validateAdvancedLoggingConfig(cfg.NotificationLogSettings.AdvancedLoggingJSON, "NotificationLogSettings.AdvancedLoggingJSON", loggingRoot) + } + + // Check ExperimentalAuditSettings.AdvancedLoggingJSON + if !utils.IsEmptyJSON(cfg.ExperimentalAuditSettings.AdvancedLoggingJSON) { + validateAdvancedLoggingConfig(cfg.ExperimentalAuditSettings.AdvancedLoggingJSON, "ExperimentalAuditSettings.AdvancedLoggingJSON", loggingRoot) + } +} + +func validateAdvancedLoggingConfig(loggingJSON json.RawMessage, configName string, loggingRoot string) { + logCfg := make(mlog.LoggerConfiguration) + if err := json.Unmarshal(loggingJSON, &logCfg); err != nil { + return + } + + for targetName, target := range logCfg { + if target.Type != "file" { + continue + } + + var fileOption struct { + Filename string `json:"filename"` + } + if err := json.Unmarshal(target.Options, &fileOption); err != nil { + continue + } + + if err := ValidateLogFilePath(fileOption.Filename, loggingRoot); err != nil { + mlog.Error("Log file path in logging config is outside logging root directory. This configuration will cause server startup to fail in a future version. To fix, set MM_LOG_PATH environment variable to a parent directory containing all log paths, or move log files to the configured logging root.", + mlog.String("config_section", configName), + mlog.String("target", targetName), + mlog.String("path", fileOption.Filename), + mlog.String("logging_root", loggingRoot), + mlog.Err(err)) + } + } +} + func makeSimpleConsoleTarget(level string, outputJSON bool, color bool) (mlog.TargetCfg, error) { levels, err := stdLevels(level) if err != nil { diff --git a/server/config/logger_test.go b/server/config/logger_test.go index fd18fce37d..2946e5c444 100644 --- a/server/config/logger_test.go +++ b/server/config/logger_test.go @@ -5,6 +5,8 @@ package config import ( "encoding/json" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -50,3 +52,169 @@ func TestMloggerConfigFromAuditConfig(t *testing.T) { assert.Equal(t, optionsExpected, optionsReceived) }) } + +func TestGetLogRootPath(t *testing.T) { + t.Run("returns MM_LOG_PATH when set", func(t *testing.T) { + // Create a temp directory to use as MM_LOG_PATH + dir, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(dir) + }) + + t.Setenv("MM_LOG_PATH", dir) + + result := GetLogRootPath() + absDir, _ := filepath.Abs(dir) + assert.Equal(t, absDir, result) + }) + + t.Run("finds logs directory relative to binary when MM_LOG_PATH not set", func(t *testing.T) { + // When MM_LOG_PATH is not set, GetLogRootPath falls back to FindDir("logs"), + // which searches for a "logs" directory relative to the working directory + // and the binary location. Create a logs directory relative to the test + // binary to verify this behavior. + t.Setenv("MM_LOG_PATH", "") + + // Get the test binary location + exe, err := os.Executable() + require.NoError(t, err) + exe, err = filepath.EvalSymlinks(exe) + require.NoError(t, err) + binaryDir := filepath.Dir(exe) + + // Create a "logs" directory next to the binary + logsDir := filepath.Join(binaryDir, "logs") + err = os.MkdirAll(logsDir, 0755) + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(logsDir) + }) + + result := GetLogRootPath() + + // Result should be an absolute path + assert.True(t, filepath.IsAbs(result), "GetLogRootPath should return an absolute path, got: %s", result) + + // FindDir searches working directory first, then binary directory. + // The result should be either the logs directory we created or another + // logs directory found earlier in the search path. Either way, it should + // be a valid directory path ending in "logs". + assert.True(t, filepath.Base(result) == "logs" || result == "./", + "GetLogRootPath should return a logs directory path, got: %s", result) + }) +} + +func TestValidateLogFilePath(t *testing.T) { + t.Run("valid path within root", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + validFile := filepath.Join(root, "app.log") + err = os.WriteFile(validFile, []byte("test"), 0644) + require.NoError(t, err) + + err = ValidateLogFilePath(validFile, root) + assert.NoError(t, err) + }) + + t.Run("valid path in subdirectory", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + subdir := filepath.Join(root, "subdir") + err = os.MkdirAll(subdir, 0755) + require.NoError(t, err) + + validFile := filepath.Join(subdir, "app.log") + err = os.WriteFile(validFile, []byte("test"), 0644) + require.NoError(t, err) + + err = ValidateLogFilePath(validFile, root) + assert.NoError(t, err) + }) + + t.Run("rejects absolute path outside root", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + outsideDir, err := os.MkdirTemp("", "outside") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(outsideDir) + }) + + outsideFile := filepath.Join(outsideDir, "secret.txt") + err = os.WriteFile(outsideFile, []byte("secret"), 0644) + require.NoError(t, err) + + err = ValidateLogFilePath(outsideFile, root) + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside logging root") + }) + + t.Run("rejects path traversal attack", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + traversalPath := filepath.Join(root, "..", "..", "etc", "passwd") + + err = ValidateLogFilePath(traversalPath, root) + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside logging root") + }) + + t.Run("rejects symlink pointing outside root", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + outsideDir, err := os.MkdirTemp("", "outside") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(outsideDir) + }) + + // Create a file outside the root + outsideFile := filepath.Join(outsideDir, "secret.txt") + err = os.WriteFile(outsideFile, []byte("secret"), 0644) + require.NoError(t, err) + + // Create a symlink inside root pointing to the outside file + symlinkPath := filepath.Join(root, "sneaky.log") + err = os.Symlink(outsideFile, symlinkPath) + require.NoError(t, err) + + err = ValidateLogFilePath(symlinkPath, root) + assert.Error(t, err) + assert.Contains(t, err.Error(), "outside logging root") + }) + + t.Run("allows non-existent file path within root", func(t *testing.T) { + root, err := os.MkdirTemp("", "logroot") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(root) + }) + + // File doesn't exist but path is within root + nonExistentFile := filepath.Join(root, "future.log") + + err = ValidateLogFilePath(nonExistentFile, root) + assert.NoError(t, err) + }) +} diff --git a/server/public/model/audit_events.go b/server/public/model/audit_events.go new file mode 100644 index 0000000000..da1f38e691 --- /dev/null +++ b/server/public/model/audit_events.go @@ -0,0 +1,473 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Access Control & Security +const ( + AuditEventApplyIPFilters = "applyIPFilters" // apply IP address filtering + AuditEventAssignAccessPolicy = "assignAccessPolicy" // assign access control policy to channels + AuditEventCreateAccessControlPolicy = "createAccessControlPolicy" // create access control policy + AuditEventDeleteAccessControlPolicy = "deleteAccessControlPolicy" // delete access control policy + AuditEventUnassignAccessPolicy = "unassignAccessPolicy" // remove access control policy from channels + AuditEventUpdateActiveStatus = "updateActiveStatus" // update active/inactive status of access control policy + AuditEventSetActiveStatus = "setActiveStatus" // set active/inactive status of multiple access control policies +) + +// Audit & Certificates +const ( + AuditEventAddAuditLogCertificate = "addAuditLogCertificate" // add certificate for secure audit log transmission + AuditEventGetAudits = "getAudits" // get audit log entries + AuditEventGetUserAudits = "getUserAudits" // get audit log entries for specific user + AuditEventRemoveAuditLogCertificate = "removeAuditLogCertificate" // remove certificate used for audit log transmission +) + +// Bots +const ( + AuditEventAssignBot = "assignBot" // assign bot to user + AuditEventConvertBotToUser = "convertBotToUser" // convert bot account to regular user account + AuditEventConvertUserToBot = "convertUserToBot" // convert regular user account to bot account + AuditEventCreateBot = "createBot" // create bot account + AuditEventPatchBot = "patchBot" // update bot properties + AuditEventUpdateBotActive = "updateBotActive" // enable or disable bot account +) + +// Branding +const ( + AuditEventDeleteBrandImage = "deleteBrandImage" // delete brand image + AuditEventUploadBrandImage = "uploadBrandImage" // upload brand image +) + +// Channel Bookmarks +const ( + AuditEventCreateChannelBookmark = "createChannelBookmark" // create bookmark in channels + AuditEventDeleteChannelBookmark = "deleteChannelBookmark" // delete bookmark + AuditEventUpdateChannelBookmark = "updateChannelBookmark" // update bookmark + AuditEventUpdateChannelBookmarkSortOrder = "updateChannelBookmarkSortOrder" // update display order of bookmarks + AuditEventListChannelBookmarksForChannel = "listChannelBookmarksForChannel" // list bookmarks for channel +) + +// Channel Categories +const ( + AuditEventCreateCategoryForTeamForUser = "createCategoryForTeamForUser" // create channel category for user + AuditEventDeleteCategoryForTeamForUser = "deleteCategoryForTeamForUser" // delete channel category + AuditEventUpdateCategoriesForTeamForUser = "updateCategoriesForTeamForUser" // update multiple channel categories + AuditEventUpdateCategoryForTeamForUser = "updateCategoryForTeamForUser" // update single channel category + AuditEventUpdateCategoryOrderForTeamForUser = "updateCategoryOrderForTeamForUser" // update display order of the categories +) + +// Channels +const ( + AuditEventAddChannelMember = "addChannelMember" // add member to channel + AuditEventConvertGroupMessageToChannel = "convertGroupMessageToChannel" // convert group message to private channel + AuditEventCreateChannel = "createChannel" // create public or private channel + AuditEventCreateDirectChannel = "createDirectChannel" // create direct message channel between two users + AuditEventCreateGroupChannel = "createGroupChannel" // create group message channel with multiple users + AuditEventDeleteChannel = "deleteChannel" // delete channel + AuditEventGetPinnedPosts = "getPinnedPosts" // get pinned posts + AuditEventLocalAddChannelMember = "localAddChannelMember" // add channel member locally + AuditEventLocalCreateChannel = "localCreateChannel" // create channel locally + AuditEventLocalDeleteChannel = "localDeleteChannel" // delete channel locally + AuditEventLocalMoveChannel = "localMoveChannel" // move channel locally + AuditEventLocalPatchChannel = "localPatchChannel" // patch channel locally + AuditEventLocalRemoveChannelMember = "localRemoveChannelMember" // remove channel member locally + AuditEventLocalRestoreChannel = "localRestoreChannel" // restore channel locally + AuditEventLocalUpdateChannelPrivacy = "localUpdateChannelPrivacy" // update channel privacy locally + AuditEventMoveChannel = "moveChannel" // move channel to different team + AuditEventPatchChannel = "patchChannel" // update channel properties + AuditEventPatchChannelModerations = "patchChannelModerations" // update channel moderation settings + AuditEventRemoveChannelMember = "removeChannelMember" // remove member from channel + AuditEventRestoreChannel = "restoreChannel" // restore previously deleted channel + AuditEventUpdateChannel = "updateChannel" // update channel properties + AuditEventUpdateChannelMemberNotifyProps = "updateChannelMemberNotifyProps" // update notification preferences + AuditEventUpdateChannelMemberRoles = "updateChannelMemberRoles" // update roles and permissions + AuditEventUpdateChannelMemberSchemeRoles = "updateChannelMemberSchemeRoles" // update scheme-based roles + AuditEventUpdateChannelPrivacy = "updateChannelPrivacy" // change channel privacy settings + AuditEventUpdateChannelScheme = "updateChannelScheme" // update permission scheme applied to channel +) + +// Commands +const ( + AuditEventCreateCommand = "createCommand" // create slash command + AuditEventDeleteCommand = "deleteCommand" // delete command + AuditEventExecuteCommand = "executeCommand" // execute command + AuditEventLocalCreateCommand = "localCreateCommand" // create command locally + AuditEventMoveCommand = "moveCommand" // move command to another team + AuditEventRegenCommandToken = "regenCommandToken" // regenerate authentication token for command + AuditEventUpdateCommand = "updateCommand" // update command +) + +// Compliance +const ( + AuditEventCreateComplianceReport = "createComplianceReport" // create compliance report + AuditEventDownloadComplianceReport = "downloadComplianceReport" // download compliance report + AuditEventGetComplianceReport = "getComplianceReport" // get specific compliance report + AuditEventGetComplianceReports = "getComplianceReports" // get all compliance reports +) + +// Configuration +const ( + AuditEventConfigReload = "configReload" // reload server configuration + AuditEventGetConfig = "getConfig" // get current server configuration + AuditEventLocalGetClientConfig = "localGetClientConfig" // get client configuration locally + AuditEventLocalGetConfig = "localGetConfig" // get server configuration locally + AuditEventLocalPatchConfig = "localPatchConfig" // update server configuration locally + AuditEventLocalUpdateConfig = "localUpdateConfig" // update server configuration locally + AuditEventMigrateConfig = "migrateConfig" // migrate configs with file values from one store to another + AuditEventPatchConfig = "patchConfig" // update server configuration + AuditEventUpdateConfig = "updateConfig" // update server configuration +) + +// Custom Profile Attributes +const ( + AuditEventCreateCPAField = "createCPAField" // create custom profile attribute + AuditEventDeleteCPAField = "deleteCPAField" // delete custom profile attribute + AuditEventPatchCPAField = "patchCPAField" // update custom profile attribute field + AuditEventPatchCPAValues = "patchCPAValues" // update custom profile attribute values +) + +// Data Retention Policies +const ( + AuditEventAddChannelsToPolicy = "addChannelsToPolicy" // add channels to data retention policy + AuditEventAddTeamsToPolicy = "addTeamsToPolicy" // add teams to data retention policy + AuditEventCreatePolicy = "createPolicy" // create data retention policy + AuditEventDeletePolicy = "deletePolicy" // delete data retention policy + AuditEventPatchPolicy = "patchPolicy" // update data retention policy + AuditEventRemoveChannelsFromPolicy = "removeChannelsFromPolicy" // remove channels from data retention policy + AuditEventRemoveTeamsFromPolicy = "removeTeamsFromPolicy" // remove teams from data retention policy +) + +// Emojis +const ( + AuditEventCreateEmoji = "createEmoji" // create emoji + AuditEventDeleteEmoji = "deleteEmoji" // delete emoji +) + +// Exports +const ( + AuditEventBulkExport = "bulkExport" // bulk export data to a file + AuditEventDeleteExport = "deleteExport" // delete exported file + AuditEventGeneratePresignURLExport = "generatePresignURLExport" // generate presigned URL to download the exported file + AuditEventScheduleExport = "scheduleExport" // schedule export job +) + +// Files +const ( + AuditEventGetFile = "getFile" // get or download file + AuditEventGetFileLink = "getFileLink" // generate link for file sharing + AuditEventUploadFileMultipart = "uploadFileMultipart" // upload file using multipart form data + AuditEventUploadFileMultipartLegacy = "uploadFileMultipartLegacy" // upload file using legacy multipart method + AuditEventUploadFileSimple = "uploadFileSimple" // upload file using simple direct upload method + AuditEventGetFileThumbnail = "getFileThumbnail" // get file thumbnail + AuditEventGetFileInfosForPost = "getFileInfosForPost" // get file infos for post + AuditEventGetFileInfo = "getFileInfo" // get file info + AuditEventGetFilePreview = "getFilePreview" // get file preview + AuditEventSearchFiles = "searchFiles" // search for files +) + +// Groups +const ( + AuditEventAddGroupMembers = "addGroupMembers" // add members to group + AuditEventAddUserToGroupSyncables = "addUserToGroupSyncables" // add user to group-synchronized teams and channels + AuditEventCreateGroup = "createGroup" // create group + AuditEventDeleteGroup = "deleteGroup" // delete group + AuditEventDeleteGroupMembers = "deleteGroupMembers" // remove members from group + AuditEventLinkGroupSyncable = "linkGroupSyncable" // link group to team or channel for synchronization + AuditEventPatchGroup = "patchGroup" // update group + AuditEventPatchGroupSyncable = "patchGroupSyncable" // update group synchronization settings + AuditEventRestoreGroup = "restoreGroup" // restore previously deleted group + AuditEventUnlinkGroupSyncable = "unlinkGroupSyncable" // unlink group from team or channel synchronization +) + +// Imports +const ( + AuditEventBulkImport = "bulkImport" // bulk import data from a file + AuditEventDeleteImport = "deleteImport" // delete import file + AuditEventSlackImport = "slackImport" // import data from Slack +) + +// Jobs +const ( + AuditEventCancelJob = "cancelJob" // cancel a job + AuditEventCreateJob = "createJob" // create a job + AuditEventJobServer = "jobServer" // start job server + AuditEventUpdateJobStatus = "updateJobStatus" // update status of a job +) + +// LDAP +const ( + AuditEventAddLdapPrivateCertificate = "addLdapPrivateCertificate" // add private certificate for LDAP + AuditEventAddLdapPublicCertificate = "addLdapPublicCertificate" // add public certificate for LDAP + AuditEventIdMigrateLdap = "idMigrateLdap" // migrate user ID mapping to another attribute + AuditEventLinkLdapGroup = "linkLdapGroup" // link LDAP group to Mattermost team or channel + AuditEventRemoveLdapPrivateCertificate = "removeLdapPrivateCertificate" // remove private certificate for LDAP + AuditEventRemoveLdapPublicCertificate = "removeLdapPublicCertificate" // remove public certificate for LDAP + AuditEventSyncLdap = "syncLdap" // synchronize users and groups from LDAP + AuditEventUnlinkLdapGroup = "unlinkLdapGroup" // unlink LDAP group from Mattermost team or channel +) + +// Licensing +const ( + AuditEventAddLicense = "addLicense" // add license + AuditEventLocalAddLicense = "localAddLicense" // add license locally + AuditEventLocalRemoveLicense = "localRemoveLicense" // remove license locally + AuditEventRemoveLicense = "removeLicense" // remove license + AuditEventRequestTrialLicense = "requestTrialLicense" // request trial license +) + +// OAuth +const ( + AuditEventAuthorizeOAuthApp = "authorizeOAuthApp" // authorize OAuth app + AuditEventAuthorizeOAuthPage = "authorizeOAuthPage" // authorize OAuth page + AuditEventCompleteOAuth = "completeOAuth" // complete OAuth authorization flow + AuditEventCreateOAuthApp = "createOAuthApp" // create OAuth app + AuditEventCreateOutgoingOauthConnection = "createOutgoingOauthConnection" // create outgoing OAuth connection + AuditEventDeauthorizeOAuthApp = "deauthorizeOAuthApp" // revoke OAuth app authorization + AuditEventDeleteOAuthApp = "deleteOAuthApp" // delete OAuth app + AuditEventDeleteOutgoingOAuthConnection = "deleteOutgoingOAuthConnection" // delete outgoing OAuth connection + AuditEventGetAccessToken = "getAccessToken" // get OAuth access token + AuditEventLoginWithOAuth = "loginWithOAuth" // login using OAuth authentication provider + AuditEventMobileLoginWithOAuth = "mobileLoginWithOAuth" // mobile application login using OAuth authentication provider + AuditEventRegenerateOAuthAppSecret = "regenerateOAuthAppSecret" // regenerate secret key for OAuth app + AuditEventRegisterOAuthClient = "registerOAuthClient" // register OAuth client via dynamic client registration (RFC 7591) + AuditEventSignupWithOAuth = "signupWithOAuth" // create account using OAuth authentication provider + AuditEventUpdateOAuthApp = "updateOAuthApp" // update OAuth app + AuditEventUpdateOutgoingOAuthConnection = "updateOutgoingOAuthConnection" // update outgoing OAuth connection + AuditEventValidateOutgoingOAuthConnectionCredentials = "validateOutgoingOAuthConnectionCredentials" // validate credentials for outgoing OAuth connection + +) + +// Plugins +const ( + AuditEventDisablePlugin = "disablePlugin" // disable installed plugin + AuditEventEnablePlugin = "enablePlugin" // enable installed plugin + AuditEventGetFirstAdminVisitMarketplaceStatus = "getFirstAdminVisitMarketplaceStatus" // get first admin visit status + AuditEventInstallMarketplacePlugin = "installMarketplacePlugin" // install plugin from official marketplace + AuditEventInstallPluginFromURL = "installPluginFromURL" // install plugin from external URL + AuditEventRemovePlugin = "removePlugin" // delete plugin + AuditEventSetFirstAdminVisitMarketplaceStatus = "setFirstAdminVisitMarketplaceStatus" // set first admin visit status + AuditEventUploadPlugin = "uploadPlugin" // upload plugin file to server for installation +) + +// Posts +const ( + AuditEventCreateEphemeralPost = "createEphemeralPost" // create ephemeral post + AuditEventCreatePost = "createPost" // create post + AuditEventDeletePost = "deletePost" // delete post + AuditEventGetEditHistoryForPost = "getEditHistoryForPost" // get edit history for post + AuditEventGetFlaggedPosts = "getFlaggedPosts" // get flagged posts + AuditEventGetPostsForChannel = "getPostsForChannel" // get posts for channel + AuditEventGetPostsForChannelAroundLastUnread = "getPostsForChannelAroundLastUnread" // get posts for channel around last unread + AuditEventGetPost = "getPost" // get post + AuditEventGetPostThread = "getPostThread" // get post thread + AuditEventGetPostsByIds = "getPostsByIds" // get posts by ids + AuditEventGetThreadForUser = "getThreadForUser" // get thread for user + AuditEventLocalDeletePost = "localDeletePost" // delete post locally + AuditEventMoveThread = "moveThread" // move thread and replies to different channel + AuditEventNotificationAck = "notificationAck" // notification ack + AuditEventPatchPost = "patchPost" // update post meta properties + AuditEventRestorePostVersion = "restorePostVersion" // restore post to previous version + AuditEventSaveIsPinnedPost = "saveIsPinnedPost" // pin or unpin post + AuditEventSearchPosts = "searchPosts" // search for posts + AuditEventUpdatePost = "updatePost" // update post content + AuditEventRevealPost = "revealPost" // reveal a post that was hidden due to burn on read + AuditEventBurnPost = "burnPost" // burn a post that was hidden due to burn on read + AuditEventWebsocketPost = "websocketPost" // post received via websocket +) + +// Recaps +const ( + AuditEventCreateRecap = "createRecap" // create recap summarizing channel content + AuditEventGetRecap = "getRecap" // view a single recap + AuditEventGetRecaps = "getRecaps" // list user's recaps + AuditEventMarkRecapAsRead = "markRecapAsRead" // mark recap as read + AuditEventRegenerateRecap = "regenerateRecap" // regenerate recap with updated channel content + AuditEventDeleteRecap = "deleteRecap" // delete recap +) + +// Preferences +const ( + AuditEventDeletePreferences = "deletePreferences" // delete user preferences + AuditEventUpdatePreferences = "updatePreferences" // update user preferences +) + +// Remote Clusters +const ( + AuditEventCreateRemoteCluster = "createRemoteCluster" // create connection to remote Mattermost cluster + AuditEventDeleteRemoteCluster = "deleteRemoteCluster" // delete connection to remote Mattermost cluster + AuditEventGenerateRemoteClusterInvite = "generateRemoteClusterInvite" // generate invitation token for remote cluster connection + AuditEventInviteRemoteClusterToChannel = "inviteRemoteClusterToChannel" // invite remote cluster users to shared channel + AuditEventPatchRemoteCluster = "patchRemoteCluster" // update remote cluster connection settings + AuditEventRemoteClusterAcceptInvite = "remoteClusterAcceptInvite" // accept invitation from remote cluster + AuditEventRemoteClusterAcceptMessage = "remoteClusterAcceptMessage" // accept message from remote cluster + AuditEventRemoteUploadProfileImage = "remoteUploadProfileImage" // upload profile image from remote cluster + AuditEventUninviteRemoteClusterToChannel = "uninviteRemoteClusterToChannel" // remove remote cluster access from shared channel + AuditEventUploadRemoteData = "uploadRemoteData" // upload data to remote cluster +) + +// Roles +const ( + AuditEventPatchRole = "patchRole" // update role permissions +) + +// SAML +const ( + AuditEventAddSamlIdpCertificate = "addSamlIdpCertificate" // add SAML identity provider certificate + AuditEventAddSamlPrivateCertificate = "addSamlPrivateCertificate" // add SAML private certificate + AuditEventAddSamlPublicCertificate = "addSamlPublicCertificate" // add SAML public certificate + AuditEventCompleteSaml = "completeSaml" // complete SAML authentication flow + AuditEventRemoveSamlIdpCertificate = "removeSamlIdpCertificate" // remove SAML identity provider certificate + AuditEventRemoveSamlPrivateCertificate = "removeSamlPrivateCertificate" // remove SAML private certificate + AuditEventRemoveSamlPublicCertificate = "removeSamlPublicCertificate" // remove SAML public certificate +) + +// Scheduled Posts +const ( + AuditEventCreateSchedulePost = "createSchedulePost" // create post scheduled for future delivery + AuditEventDeleteScheduledPost = "deleteScheduledPost" // delete scheduled post before delivery + AuditEventUpdateScheduledPost = "updateScheduledPost" // update scheduled post +) + +// Schemes +const ( + AuditEventCreateScheme = "createScheme" // create permission scheme with role definitions + AuditEventDeleteScheme = "deleteScheme" // delete scheme + AuditEventPatchScheme = "patchScheme" // update scheme +) + +// Search Indexes +const ( + AuditEventPurgeBleveIndexes = "purgeBleveIndexes" // purge Bleve search indexes + AuditEventPurgeElasticsearchIndexes = "purgeElasticsearchIndexes" // purge Elasticsearch search indexes +) + +// Server Administration +const ( + AuditEventClearServerBusy = "clearServerBusy" // clear server busy status to allow normal operations + AuditEventCompleteOnboarding = "completeOnboarding" // complete system onboarding process + AuditEventDatabaseRecycle = "databaseRecycle" // closes active connections + AuditEventDownloadLogs = "downloadLogs" // download server log files + AuditEventGenerateSupportPacket = "generateSupportPacket" // generate support packet with server diagnostics and logs + AuditEventGetAppliedSchemaMigrations = "getAppliedSchemaMigrations" // get list of applied database schema migrations + AuditEventGetLogs = "getLogs" // get server log entries + AuditEventGetOnboarding = "getOnboarding" // get system onboarding status + AuditEventInvalidateCaches = "invalidateCaches" // clear server caches + AuditEventLocalCheckIntegrity = "localCheckIntegrity" // check database integrity locally + AuditEventQueryLogs = "queryLogs" // search server log entries + AuditEventRestartServer = "restartServer" // restart Mattermost server process + AuditEventSetServerBusy = "setServerBusy" // set server busy status to disallow any operations + AuditEventUpdateViewedProductNotices = "updateViewedProductNotices" // update viewed status of product notices + AuditEventUpgradeToEnterprise = "upgradeToEnterprise" // upgrade server to Enterprise edition +) + +// Teams +const ( + AuditEventAddTeamMember = "addTeamMember" // add member to team + AuditEventAddTeamMembers = "addTeamMembers" // add multiple members to team + AuditEventAddUserToTeamFromInvite = "addUserToTeamFromInvite" // add user to team using invitation link + AuditEventCreateTeam = "createTeam" // create team + AuditEventDeleteTeam = "deleteTeam" // delete team + AuditEventImportTeam = "importTeam" // import team data from external source + AuditEventInvalidateAllEmailInvites = "invalidateAllEmailInvites" // invalidate all pending email invitations + AuditEventInviteGuestsToChannels = "inviteGuestsToChannels" // invite guest users to specific channels + AuditEventInviteUsersToTeam = "inviteUsersToTeam" // invite users to team + AuditEventLocalCreateTeam = "localCreateTeam" // create team locally + AuditEventLocalDeleteTeam = "localDeleteTeam" // delete team locally + AuditEventLocalInviteUsersToTeam = "localInviteUsersToTeam" // invite users to team locally + AuditEventPatchTeam = "patchTeam" // update team properties + AuditEventRegenerateTeamInviteId = "regenerateTeamInviteId" // regenerate team invitation ID + AuditEventRemoveTeamIcon = "removeTeamIcon" // remove custom icon from team + AuditEventRemoveTeamMember = "removeTeamMember" // remove member from team + AuditEventRestoreTeam = "restoreTeam" // restore previously deleted team + AuditEventSetTeamIcon = "setTeamIcon" // set custom icon for team + AuditEventUpdateTeam = "updateTeam" // update team properties + AuditEventUpdateTeamMemberRoles = "updateTeamMemberRoles" // update roles of team members + AuditEventUpdateTeamMemberSchemeRoles = "updateTeamMemberSchemeRoles" // update scheme-based roles of team members + AuditEventUpdateTeamPrivacy = "updateTeamPrivacy" // change team privacy settings + AuditEventUpdateTeamScheme = "updateTeamScheme" // update scheme applied to team +) + +// Terms of Service +const ( + AuditEventCreateTermsOfService = "createTermsOfService" // create terms of service + AuditEventSaveUserTermsOfService = "saveUserTermsOfService" // save user acceptance of terms of service +) + +// Threads +const ( + AuditEventFollowThreadByUser = "followThreadByUser" // follow thread to receive notifications about replies + AuditEventSetUnreadThreadByPostId = "setUnreadThreadByPostId" // mark thread as unread for user by post ID + AuditEventUnfollowThreadByUser = "unfollowThreadByUser" // unfollow thread to stop receiving notifications about replies + AuditEventUpdateReadStateAllThreadsByUser = "updateReadStateAllThreadsByUser" // update read status for all threads for user + AuditEventUpdateReadStateThreadByUser = "updateReadStateThreadByUser" // update read status for specific thread for user +) + +// Uploads +const ( + AuditEventCreateUpload = "createUpload" // create file upload session + AuditEventUploadData = "uploadData" // upload file data to server storage +) + +// Users +const ( + AuditEventAttachDeviceId = "attachDeviceId" // attach device ID to user session for mobile app + AuditEventCreateUser = "createUser" // create user account + AuditEventCreateUserAccessToken = "createUserAccessToken" // create personal access token for user API access + AuditEventDeleteUser = "deleteUser" // delete user account + AuditEventDemoteUserToGuest = "demoteUserToGuest" // demote regular user to guest account with limited permissions + AuditEventDisableUserAccessToken = "disableUserAccessToken" // disable user personal access token + AuditEventEnableUserAccessToken = "enableUserAccessToken" // enable user personal access token + AuditEventExtendSessionExpiry = "extendSessionExpiry" // extend user session expiration time + AuditEventLocalDeleteUser = "localDeleteUser" // delete user locally + AuditEventLocalPermanentDeleteAllUsers = "localPermanentDeleteAllUsers" // permanently delete all users locally + AuditEventLogin = "login" // user login to system + AuditEventLoginWithDesktopToken = "loginWithDesktopToken" // user login to system with desktop token + AuditEventLogout = "logout" // user logout from system + AuditEventMigrateAuthToLdap = "migrateAuthToLdap" // migrate user authentication method to LDAP + AuditEventMigrateAuthToSaml = "migrateAuthToSaml" // migrate user authentication method to SAML + AuditEventPatchUser = "patchUser" // update user properties + AuditEventPromoteGuestToUser = "promoteGuestToUser" // promote guest account to regular user + AuditEventResetPassword = "resetPassword" // reset user password + AuditEventResetPasswordFailedAttempts = "resetPasswordFailedAttempts" // reset failed password attempt counter + AuditEventRevokeAllSessionsAllUsers = "revokeAllSessionsAllUsers" // revoke all active sessions for all users + AuditEventRevokeAllSessionsForUser = "revokeAllSessionsForUser" // revoke all active sessions for specific user + AuditEventRevokeSession = "revokeSession" // revoke specific user session + AuditEventRevokeUserAccessToken = "revokeUserAccessToken" // revoke user personal access token + AuditEventSendPasswordReset = "sendPasswordReset" // send password reset email to user + AuditEventSendVerificationEmail = "sendVerificationEmail" // send email verification link to user + AuditEventSetDefaultProfileImage = "setDefaultProfileImage" // set user profile image to default avatar + AuditEventSetProfileImage = "setProfileImage" // set custom profile image for user + AuditEventSwitchAccountType = "switchAccountType" // switch user authentication method from one to another + AuditEventUpdatePassword = "updatePassword" // update user password + AuditEventUpdateUser = "updateUser" // update user account properties + AuditEventUpdateUserActive = "updateUserActive" // update user active status + AuditEventUpdateUserAuth = "updateUserAuth" // update user authentication method + AuditEventUpdateUserMfa = "updateUserMfa" // update user multi-factor authentication settings + AuditEventUpdateUserRoles = "updateUserRoles" // update user roles + AuditEventVerifyUserEmail = "verifyUserEmail" // verify user email address using verification token + AuditEventVerifyUserEmailWithoutToken = "verifyUserEmailWithoutToken" // verify user email address without verification token +) + +// Webhooks +const ( + AuditEventCreateIncomingHook = "createIncomingHook" // create incoming webhook + AuditEventCreateOutgoingHook = "createOutgoingHook" // create outgoing webhook + AuditEventDeleteIncomingHook = "deleteIncomingHook" // delete incoming webhook + AuditEventDeleteOutgoingHook = "deleteOutgoingHook" // delete outgoing webhook + AuditEventGetIncomingHook = "getIncomingHook" // get incoming webhook details + AuditEventGetOutgoingHook = "getOutgoingHook" // get outgoing webhook details + AuditEventLocalCreateIncomingHook = "localCreateIncomingHook" // create incoming webhook locally + AuditEventRegenOutgoingHookToken = "regenOutgoingHookToken" // regenerate authentication token + AuditEventUpdateIncomingHook = "updateIncomingHook" // update incoming webhook + AuditEventUpdateOutgoingHook = "updateOutgoingHook" // update outgoing webhook +) + +// Content Flagging +const ( + AuditEventFlagPost = "flagPost" // flag post for review + AuditEventGetFlaggedPost = "getFlaggedPost" // get flagged post details + AuditEventPermanentlyRemoveFlaggedPost = "permanentlyRemoveFlaggedPost" // permanently remove flagged post + AuditEventKeepFlaggedPost = "keepFlaggedPost" // keep flagged post + AuditEventUpdateContentFlaggingConfig = "updateContentFlaggingConfig" // update content flagging configuration + AuditEventSetReviewer = "setFlaggedPostReviewer" // assign reviewer for flagged post +)