Files
mostlymatter/server/channels/app/platform/log_test.go

492 строки
15 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"os"
"path"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetMattermostLog(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
// disable mattermost log file setting in config so we should get an warning
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.LogSettings.EnableFile = false
})
fileData, err := th.Service.GetLogFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false")
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() {
// MM-62438: 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.LogSettings.EnableFile = false
})
th.Service.Logger().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 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
*cfg.LogSettings.FileLocation = dir
})
logLocation := config.GetLogFileLocation(dir)
// There is no mattermost.log file yet, so this fails
fileData, err = th.Service.GetLogFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation)
// Happy path where we get a log file and no warning
d1 := []byte("hello\ngo\n")
err = os.WriteFile(logLocation, d1, 0777)
require.NoError(t, err)
fileData, err = th.Service.GetLogFile(th.Context)
require.NoError(t, err)
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) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
// Disable notifications file setting in config so we should get an warning
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.NotificationLogSettings.EnableFile = false
})
fileData, err := th.Service.GetNotificationLogFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false")
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
*cfg.NotificationLogSettings.FileLocation = dir
})
logLocation := config.GetNotificationsLogFileLocation(dir)
// There is no notifications.log file yet, so this fails
fileData, err = th.Service.GetNotificationLogFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "failed read notifcation log file at path "+logLocation)
// Happy path where we have file and no error
d1 := []byte("hello\ngo\n")
err = os.WriteFile(logLocation, d1, 0777)
require.NoError(t, err)
fileData, err = th.Service.GetNotificationLogFile(th.Context)
assert.NoError(t, err)
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 TestGetLogsSkipSendPathValidation(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
t.Run("path validation prevents reading files outside log directory", func(t *testing.T) {
// Create a directory to use as the allowed log root
logDir, err := os.MkdirTemp("", "logs")
require.NoError(t, err)
t.Cleanup(func() {
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.LogSettings.EnableFile = false
})
th.Service.Logger().Flush()
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 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 log file outside the allowed root that should not be readable
outsideLogLocation := config.GetLogFileLocation(outsideDir)
err = os.WriteFile(outsideLogLocation, []byte("secret data\n"), 0644)
require.NoError(t, err)
// Point FileLocation to the outside directory
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.LogSettings.EnableFile = true
*cfg.LogSettings.FileLocation = outsideDir
})
// Should be blocked by path validation
lines, appErr := th.Service.GetLogsSkipSend(th.Context, 0, 10, &model.LogFilter{})
assert.Nil(t, lines)
require.NotNil(t, appErr)
assert.Equal(t, "api.admin.file_read_error", appErr.Id)
})
}
func TestGetAdvancedLogs(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
t.Run("log messages from advanced logging settings get returned", func(t *testing.T) {
dir, err := os.MkdirTemp("", "logs")
require.NoError(t, err)
t.Cleanup(func() {
err = os.RemoveAll(dir)
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"),
}
dataLDAP, err := json.Marshal(optLDAP)
require.NoError(t, err)
optStd := map[string]string{
"filename": path.Join(dir, "std.log"),
}
dataStd, err := json.Marshal(optStd)
require.NoError(t, err)
optNotif := map[string]string{
"filename": path.Join(dir, "notification.log"),
}
dataNotif, err := json.Marshal(optNotif)
require.NoError(t, err)
// LogSettings config
logCfg := mlog.LoggerConfiguration{
"ldap-file": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlLDAPError,
mlog.LvlLDAPWarn,
mlog.LvlLDAPInfo,
mlog.LvlLDAPDebug,
},
Options: dataLDAP,
},
"std": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlError,
},
Options: dataStd,
},
}
logCfgData, err := json.Marshal(logCfg)
require.NoError(t, err)
// NotificationLogSettings config
notifCfg := mlog.LoggerConfiguration{
"notification": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlInfo,
},
Options: dataNotif,
},
}
notifCfgData, err := json.Marshal(notifCfg)
require.NoError(t, err)
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = logCfgData
c.NotificationLogSettings.AdvancedLoggingJSON = notifCfgData
// Audit logs are not testiable as they as part of the server, not the platform
})
// Write some logs and ensure they're flushed
logger := th.Service.Logger()
notifLogger := th.Service.NotificationsLogger()
logger.LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
logger.Error("Some Error")
notifLogger.Info("Some Notification")
// Flush both loggers and wait a bit for filesystem
err = logger.Flush()
require.NoError(t, err)
err = notifLogger.Flush()
require.NoError(t, err)
// Get and verify logs
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
require.NoError(t, err)
for _, fd := range fileDatas {
t.Log(fd.Filename)
}
require.Len(t, fileDatas, 3)
// Helper to find file data by name
findFile := func(name string) *model.FileData {
for _, fd := range fileDatas {
if fd.Filename == name {
return fd
}
}
return nil
}
// Check each log file
ldapFile := findFile("ldap.log")
require.NotNil(t, ldapFile)
testlib.AssertLog(t, bytes.NewBuffer(ldapFile.Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
stdFile := findFile("std.log")
require.NotNil(t, stdFile)
testlib.AssertLog(t, bytes.NewBuffer(stdFile.Body), mlog.LvlError.Name, "Some Error")
notifFile := findFile("notification.log")
require.NotNil(t, notifFile)
testlib.AssertLog(t, bytes.NewBuffer(notifFile.Body), mlog.LvlInfo.Name, "Some Notification")
})
// 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
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
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)
})
}