Cherry-pick MM-66789: Restrict log downloads to a root path for support packets (#35164)
Automatic Merge
Этот коммит содержится в:
@@ -106,7 +106,14 @@ func setupTestHelper(tb testing.TB, dbStore store.Store, sqlSettings *model.SqlS
|
|||||||
consoleLevel = mlog.LvlStdLog.Name
|
consoleLevel = mlog.LvlStdLog.Name
|
||||||
}
|
}
|
||||||
*memoryConfig.LogSettings.ConsoleLevel = consoleLevel
|
*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.AdminNoticesEnabled = false
|
||||||
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
|
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
|
||||||
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
|
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auditRec := c.MakeAuditRecord(model.AuditEventGenerateSupportPacket, model.AuditStatusFail)
|
||||||
|
defer c.LogAuditRec(auditRec)
|
||||||
|
|
||||||
// We support the existing API hence the logs are always included
|
// We support the existing API hence the logs are always included
|
||||||
// if nothing specified.
|
// if nothing specified.
|
||||||
includeLogs := true
|
includeLogs := true
|
||||||
@@ -99,6 +102,9 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
PluginPackets: r.Form["plugin_packets"],
|
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)
|
// 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 {
|
if c.App.Channels().License() == nil {
|
||||||
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden)
|
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)
|
fileBytesReader := bytes.NewReader(fileBytes)
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
auditRec.AddMeta("filename", outputZipFilename)
|
||||||
|
|
||||||
// Prevent caching so support packets are always fresh
|
// Prevent caching so support packets are always fresh
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ func setupTestHelper(dbStore store.Store, sqlStore *sqlstore.SqlStore, sqlSettin
|
|||||||
*memoryConfig.LogSettings.ConsoleLevel = mlog.LvlStdLog.Name
|
*memoryConfig.LogSettings.ConsoleLevel = mlog.LvlStdLog.Name
|
||||||
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
|
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
|
||||||
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = 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 {
|
if updateConfig != nil {
|
||||||
updateConfig(memoryConfig)
|
updateConfig(memoryConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
oldCfg, newCfg, err := ps.configStore.Set(newCfg)
|
||||||
if errors.Is(err, config.ErrReadOnlyConfiguration) {
|
if errors.Is(err, config.ErrReadOnlyConfiguration) {
|
||||||
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err)
|
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err)
|
||||||
|
|||||||
@@ -215,12 +215,22 @@ func (ps *PlatformService) GetLogsSkipSend(rctx request.CTX, page, perPage int,
|
|||||||
return lines, nil
|
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 {
|
if !*ps.Config().LogSettings.EnableFile {
|
||||||
return nil, errors.New("Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false")
|
return nil, errors.New("Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false")
|
||||||
}
|
}
|
||||||
|
|
||||||
mattermostLog := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation)
|
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)
|
mattermostLogFileData, err := os.ReadFile(mattermostLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog)
|
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
|
}, 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 {
|
if !*ps.Config().NotificationLogSettings.EnableFile {
|
||||||
return nil, errors.New("Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false")
|
return nil, errors.New("Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false")
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsLog := config.GetNotificationsLogFileLocation(*ps.Config().NotificationLogSettings.FileLocation)
|
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)
|
notificationsLogFileData, err := os.ReadFile(notificationsLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog)
|
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
|
}, 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 (
|
var (
|
||||||
rErr *multierror.Error
|
rErr *multierror.Error
|
||||||
ret []*model.FileData
|
ret []*model.FileData
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rctx.Logger().Debug("Advanced logs access requested")
|
||||||
|
|
||||||
for name, loggingJSON := range map[string]json.RawMessage{
|
for name, loggingJSON := range map[string]json.RawMessage{
|
||||||
"LogSettings.AdvancedLoggingJSON": ps.Config().LogSettings.AdvancedLoggingJSON,
|
"LogSettings.AdvancedLoggingJSON": ps.Config().LogSettings.AdvancedLoggingJSON,
|
||||||
"NotificationLogSettings.AdvancedLoggingJSON": ps.Config().NotificationLogSettings.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))
|
rErr = multierror.Append(rErr, errors.Wrapf(err, "error decoding file target options in %s", name))
|
||||||
continue
|
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)
|
data, err := os.ReadFile(fileOption.Filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rErr = multierror.Append(rErr, errors.Wrapf(err, "failed to read advanced log file at path %s in %s", fileOption.Filename, name))
|
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 {
|
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ func TestGetMattermostLog(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
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
|
// 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) {
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.LogSettings.EnableFile = true
|
*cfg.LogSettings.EnableFile = true
|
||||||
@@ -71,6 +74,33 @@ func TestGetMattermostLog(t *testing.T) {
|
|||||||
require.NotNil(t, fileData)
|
require.NotNil(t, fileData)
|
||||||
assert.Equal(t, "mattermost.log", fileData.Filename)
|
assert.Equal(t, "mattermost.log", fileData.Filename)
|
||||||
assert.Positive(t, len(fileData.Body))
|
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) {
|
func TestGetNotificationLogFile(t *testing.T) {
|
||||||
@@ -91,10 +121,20 @@ func TestGetNotificationLogFile(t *testing.T) {
|
|||||||
dir, err := os.MkdirTemp("", "")
|
dir, err := os.MkdirTemp("", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
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)
|
err = os.RemoveAll(dir)
|
||||||
assert.NoError(t, err)
|
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
|
// 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) {
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.NotificationLogSettings.EnableFile = true
|
*cfg.NotificationLogSettings.EnableFile = true
|
||||||
@@ -118,6 +158,33 @@ func TestGetNotificationLogFile(t *testing.T) {
|
|||||||
require.NotNil(t, fileData)
|
require.NotNil(t, fileData)
|
||||||
assert.Equal(t, "notifications.log", fileData.Filename)
|
assert.Equal(t, "notifications.log", fileData.Filename)
|
||||||
assert.Positive(t, len(fileData.Body))
|
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) {
|
func TestGetAdvancedLogs(t *testing.T) {
|
||||||
@@ -134,6 +201,9 @@ func TestGetAdvancedLogs(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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
|
// Setup log files for each setting
|
||||||
optLDAP := map[string]string{
|
optLDAP := map[string]string{
|
||||||
"filename": path.Join(dir, "ldap.log"),
|
"filename": path.Join(dir, "ldap.log"),
|
||||||
@@ -243,9 +313,10 @@ func TestGetAdvancedLogs(t *testing.T) {
|
|||||||
require.NotNil(t, notifFile)
|
require.NotNil(t, notifFile)
|
||||||
testlib.AssertLog(t, bytes.NewBuffer(notifFile.Body), mlog.LvlInfo.Name, "Some Notification")
|
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) {
|
th.Service.UpdateConfig(func(c *model.Config) {
|
||||||
c.LogSettings.AdvancedLoggingJSON = nil
|
c.LogSettings.AdvancedLoggingJSON = nil
|
||||||
|
c.NotificationLogSettings.AdvancedLoggingJSON = nil
|
||||||
})
|
})
|
||||||
t.Run("No logs returned when AdvancedLoggingJSON is empty", func(t *testing.T) {
|
t.Run("No logs returned when AdvancedLoggingJSON is empty", func(t *testing.T) {
|
||||||
// Confirm no logs get returned
|
// Confirm no logs get returned
|
||||||
@@ -253,4 +324,119 @@ func TestGetAdvancedLogs(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, fileDatas, 0)
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
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) {
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.LogSettings.FileLocation = dir
|
*cfg.LogSettings.FileLocation = dir
|
||||||
*cfg.NotificationLogSettings.FileLocation = dir
|
*cfg.NotificationLogSettings.FileLocation = dir
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ func TestGenerateSupportPacket(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
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) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.LogSettings.FileLocation = dir
|
*cfg.LogSettings.FileLocation = dir
|
||||||
*cfg.NotificationLogSettings.FileLocation = dir
|
*cfg.NotificationLogSettings.FileLocation = dir
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type MainHelper struct {
|
|||||||
|
|
||||||
status int
|
status int
|
||||||
testResourcePath string
|
testResourcePath string
|
||||||
|
testLogsPath string
|
||||||
replicas []string
|
replicas []string
|
||||||
storePool *sqlstore.TestPool
|
storePool *sqlstore.TestPool
|
||||||
}
|
}
|
||||||
@@ -88,6 +89,15 @@ func NewMainHelperWithOptions(options *HelperOptions) *MainHelper {
|
|||||||
log.Fatal(err)
|
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 {
|
if options != nil {
|
||||||
mainHelper.Options = *options
|
mainHelper.Options = *options
|
||||||
|
|
||||||
@@ -293,6 +303,10 @@ func (h *MainHelper) Close() error {
|
|||||||
if h.testResourcePath != "" {
|
if h.testResourcePath != "" {
|
||||||
os.RemoveAll(h.testResourcePath)
|
os.RemoveAll(h.testResourcePath)
|
||||||
}
|
}
|
||||||
|
if h.testLogsPath != "" {
|
||||||
|
os.RemoveAll(h.testLogsPath)
|
||||||
|
os.Unsetenv("MM_LOG_PATH")
|
||||||
|
}
|
||||||
|
|
||||||
if h.storePool != nil {
|
if h.storePool != nil {
|
||||||
h.storePool.Close()
|
h.storePool.Close()
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ func SetupTestResources() (string, error) {
|
|||||||
return "", errors.Wrapf(err, "failed to create client directory %s", clientDir)
|
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"))
|
err = setupConfig(path.Join(tempDir, "config"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to setup config")
|
return "", errors.Wrap(err, "failed to setup config")
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ package config
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
|
"github.com/mattermost/mattermost/server/public/utils"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,6 +129,123 @@ func GetLogSettingsFromNotificationsLogSettings(notificationLogSettings *model.N
|
|||||||
return settings
|
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) {
|
func makeSimpleConsoleTarget(level string, outputJSON bool, color bool) (mlog.TargetCfg, error) {
|
||||||
levels, err := stdLevels(level)
|
levels, err := stdLevels(level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -50,3 +52,169 @@ func TestMloggerConfigFromAuditConfig(t *testing.T) {
|
|||||||
assert.Equal(t, optionsExpected, optionsReceived)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
473
server/public/model/audit_events.go
Обычный файл
473
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
|
||||||
|
)
|
||||||
Ссылка в новой задаче
Block a user