[MM-54288] Support Packet V2 (#29403)

This commit is contained in:
Ben Schumacher 2025-01-13 20:23:09 +01:00 committed by GitHub
parent 091d1bba8b
commit 8d4bf4bae0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2429 additions and 745 deletions

View file

@ -8,7 +8,7 @@ import (
"os"
"strings"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
type DockerCompose struct {

View file

@ -20,6 +20,7 @@ import (
"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/audit"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
@ -112,14 +113,10 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
fileDatas := c.App.GenerateSupportPacket(c.AppContext, supportPacketOptions)
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
// Note that this filename is also being checked at the webapp, please update the
// regex within the commercial_support_modal.tsx file if the naming convention ever changes.
now := time.Now()
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))
outputZipFilename := supportPacketFileName(now, c.App.License().Customer.Company)
fileStorageBackend := c.App.FileBackend()
// We do this incase we get concurrent requests, we will always have a unique directory.
// This is to avoid the situation where we try to write to the same directory while we are trying to delete it (further down)
outputDirectoryToUse := OutputDirectory + "_" + model.NewId()
@ -147,6 +144,13 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
web.WriteFileResponse(outputZipFilename, FileMime, 0, now, *c.App.Config().ServiceSettings.WebserverMode, fileBytesReader, true, w, r)
}
// supportPacketFileName returns the ZIP file name in the format mm_support_packet_$CUSTOMER_NAME_YYYY-MM-DDTHH-MM.zip.
// Note that this filename is also being checked at the webapp, please update the
// regex within the commercial_support_modal.tsx file if the naming convention ever changes.
func supportPacketFileName(now time.Time, customerName string) string {
return fmt.Sprintf("mm_support_packet_%s_%s.zip", utils.SanitizeFileName(customerName), now.Format("2006-01-02T15-04"))
}
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
reqs := c.App.Config().ClientRequirements

View file

@ -233,9 +233,14 @@ func TestGenerateSupportPacket(t *testing.T) {
th.App.Srv().SetLicense(l)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
file, _, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
file, filename, _, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
require.NoError(t, err)
require.NotZero(t, len(file))
assert.Contains(t, filename, "mm_support_packet_My_awesome_Company_")
d, err := io.ReadAll(file)
require.NoError(t, err)
assert.NotZero(t, len(d))
})
})
@ -249,20 +254,20 @@ func TestGenerateSupportPacket(t *testing.T) {
}()
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
_, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
_, _, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
})
t.Run("As a system role, not system admin", func(t *testing.T) {
_, resp, err := th.SystemManagerClient.GenerateSupportPacket(context.Background())
_, _, resp, err := th.SystemManagerClient.GenerateSupportPacket(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("As a Regular User", func(t *testing.T) {
_, resp, err := th.Client.GenerateSupportPacket(context.Background())
_, _, resp, err := th.Client.GenerateSupportPacket(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
@ -271,12 +276,43 @@ func TestGenerateSupportPacket(t *testing.T) {
_, err := th.SystemAdminClient.RemoveLicenseFile(context.Background())
require.NoError(t, err)
_, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
_, _, resp, err := th.SystemAdminClient.GenerateSupportPacket(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestSupportPacketFileName(t *testing.T) {
tests := map[string]struct {
now time.Time
customerName string
expected string
}{
"standard case": {
now: time.Date(2023, 11, 12, 13, 14, 15, 0, time.UTC),
customerName: "TestCustomer",
expected: "mm_support_packet_TestCustomer_2023-11-12T13-14.zip",
},
"customer name with special characters": {
now: time.Date(2023, 11, 12, 13, 14, 15, 0, time.UTC),
customerName: "Test/Customer:Name",
expected: "mm_support_packet_Test_Customer_Name_2023-11-12T13-14.zip",
},
"empty customer name": {
now: time.Date(2023, 10, 10, 10, 10, 10, 0, time.UTC),
customerName: "",
expected: "mm_support_packet__2023-10-10T10-10.zip",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
result := supportPacketFileName(tt.now, tt.customerName)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSiteURLTest(t *testing.T) {
th := Setup(t)
defer th.TearDown()

View file

@ -87,6 +87,9 @@ func (a *App) SearchEngine() *searchengine.Broker {
func (a *App) Ldap() einterfaces.LdapInterface {
return a.ch.Ldap
}
func (a *App) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
return a.ch.srv.platform.LdapDiagnostic()
}
func (a *App) MessageExport() einterfaces.MessageExportInterface {
return a.ch.MessageExport
}

View file

@ -582,6 +582,7 @@ type AppIface interface {
DeleteReactionForPost(c request.CTX, reaction *model.Reaction) *model.AppError
DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError)
DeleteRetentionPolicy(policyID string) *model.AppError
DeleteRole(id string) (*model.Role, *model.AppError)
DeleteScheduledPost(rctx request.CTX, userId, scheduledPostId, connectionId string) (*model.ScheduledPost, *model.AppError)
DeleteScheme(schemeId string) (*model.Scheme, *model.AppError)
DeleteSharedChannelRemote(id string) (bool, error)
@ -622,7 +623,7 @@ type AppIface interface {
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError)
GeneratePublicLink(siteURL string, info *model.FileInfo) string
GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData
GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData
GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError)
GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError)
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)
@ -947,6 +948,7 @@ type AppIface interface {
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError)
Ldap() einterfaces.LdapInterface
LdapDiagnostic() einterfaces.LdapDiagnosticInterface
LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError
LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError
License() *model.License

View file

@ -39,7 +39,7 @@ func (a *App) SyncLdap(c request.CTX, includeRemovedMembers bool) {
func (a *App) TestLdap(rctx request.CTX) *model.AppError {
license := a.Srv().License()
if ldapI := a.Ldap(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
if ldapI := a.LdapDiagnostic(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
if err := ldapI.RunTest(rctx); err != nil {
err.StatusCode = 500
return err

View file

@ -3771,6 +3771,28 @@ func (a *OpenTracingAppLayer) DeleteRetentionPolicy(policyID string) *model.AppE
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteRole(id string) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteRole(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteScheduledPost(rctx request.CTX, userId string, scheduledPostId string, connectionId string) (*model.ScheduledPost, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheduledPost")
@ -4961,7 +4983,7 @@ func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.Fil
return resultVar0
}
func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
func (a *OpenTracingAppLayer) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
@ -4973,7 +4995,7 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX, options *mode
}()
defer span.Finish()
resultVar0 := a.app.GenerateSupportPacket(c, options)
resultVar0 := a.app.GenerateSupportPacket(rctx, options)
return resultVar0
}
@ -12641,6 +12663,23 @@ func (a *OpenTracingAppLayer) JoinUserToTeam(c request.CTX, team *model.Team, us
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LdapDiagnostic")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LdapDiagnostic()
return resultVar0
}
func (a *OpenTracingAppLayer) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveChannel")

View file

@ -21,6 +21,7 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/config"
@ -40,6 +41,23 @@ func (ps *PlatformService) Config() *model.Config {
return ps.configStore.Get()
}
// getSanitizedConfig gets the configuration without any secrets.
func (ps *PlatformService) getSanitizedConfig(rctx request.CTX) *model.Config {
cfg := ps.Config().Clone()
manifests, err := ps.getPluginManifests()
if err != nil {
// getPluginManifests might error, e.g. when plugins are disabled.
// Sanitize all plugin settings in this case.
rctx.Logger().Warn("Failed to get plugin manifests for config sanitization. Will sanitize all plugin settings.", mlog.Err(err))
cfg.Sanitize(nil)
} else {
cfg.Sanitize(manifests)
}
return cfg
}
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
// for the listener that can later be used to remove it.

View file

@ -20,6 +20,12 @@ func RegisterElasticsearchInterface(f func(*PlatformService) searchengine.Search
elasticsearchInterface = f
}
var ldapDiagnosticInterface func(*PlatformService) einterfaces.LdapDiagnosticInterface
func RegisterLdapDiagnosticInterface(f func(*PlatformService) einterfaces.LdapDiagnosticInterface) {
ldapDiagnosticInterface = f
}
var licenseInterface func(*PlatformService) einterfaces.LicenseInterface
func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterface) {

View file

@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
@ -143,7 +144,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
*memoryConfig.MetricsSettings.Enable = true
*memoryConfig.ServiceSettings.ListenAddress = "localhost:0"
*memoryConfig.MetricsSettings.ListenAddress = "localhost:0"
configStore.Set(memoryConfig)
_, _, err = configStore.Set(memoryConfig)
require.NoError(tb, err)
options = append(options, ConfigStore(configStore))
@ -152,7 +154,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
Store: dbStore,
}, options...)
if err != nil {
panic(err)
require.NoError(tb, err)
}
th := &TestHelper{

View file

@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
"path"
"time"
"github.com/pkg/errors"
@ -16,6 +17,7 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/config"
)
@ -246,6 +248,44 @@ func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileDat
}, nil
}
func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, error) {
advancedLoggingJSON := ps.Config().LogSettings.AdvancedLoggingJSON
if utils.IsEmptyJSON(advancedLoggingJSON) {
return nil, nil
}
cfg := make(mlog.LoggerConfiguration)
err := json.Unmarshal(advancedLoggingJSON, &cfg)
if err != nil {
return nil, errors.Wrap(err, "invalid advanced logging configuration")
}
var ret []*model.FileData
for _, t := range cfg {
if t.Type != "file" {
continue
}
var fileOption struct {
Filename string `json:"filename"`
}
if err := json.Unmarshal(t.Options, &fileOption); err != nil {
return nil, errors.Wrap(err, "error decoding file target options")
}
data, err := os.ReadFile(fileOption.Filename)
if err != nil {
return nil, errors.Wrapf(err, "failed to read notifcation log file at path %s", fileOption.Filename)
}
fileName := path.Base(fileOption.Filename)
ret = append(ret, &model.FileData{
Filename: fileName,
Body: data,
})
}
return ret, nil
}
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
logLevels := logFilter.LogLevels
if len(logLevels) == 0 {

View file

@ -4,10 +4,15 @@
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"
@ -104,3 +109,89 @@ func TestGetNotificationLogFile(t *testing.T) {
assert.Equal(t, "notifications.log", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetAdvancedLogs(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("log messanges from std and LDAP level 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)
})
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)
cfg := 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,
},
}
cfgData, err := json.Marshal(cfg)
require.NoError(t, err)
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = cfgData
})
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
th.Service.Logger().Error("Some Error")
err = th.Service.Logger().Flush()
require.NoError(t, err)
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
require.NoError(t, err)
require.Len(t, fileDatas, 2)
// Check the order of the log files
var ldapIndex = 0
var stdIndex = 1
if fileDatas[1].Filename == "ldap.log" {
ldapIndex = 1
stdIndex = 0
}
assert.Equal(t, "ldap.log", fileDatas[ldapIndex].Filename)
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[ldapIndex].Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
assert.Equal(t, "std.log", fileDatas[stdIndex].Filename)
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[stdIndex].Body), mlog.LvlError.Name, "Some Error")
})
// Disable AdvancedLoggingJSON
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.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)
})
}

View file

@ -4,7 +4,6 @@
package platform
import (
"bytes"
"context"
"fmt"
"net"
@ -12,7 +11,6 @@ import (
"net/http/pprof"
"path"
"runtime"
rpprof "runtime/pprof"
"strings"
"sync"
"text/template"
@ -25,7 +23,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
@ -259,52 +256,3 @@ func (ps *PlatformService) Metrics() einterfaces.MetricsInterface {
return ps.metricsIFace
}
func (ps *PlatformService) CreateCPUProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.StartCPUProfile(&b)
if err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
time.Sleep(cpuProfileDuration)
rpprof.StopCPUProfile()
fileData := &model.FileData{
Filename: "cpu.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) CreateHeapProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("heap").WriteTo(&b, 0)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup heap profile")
}
fileData := &model.FileData{
Filename: "heap.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) CreateGoroutineProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("goroutine").WriteTo(&b, 2)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup goroutine profile")
}
fileData := &model.FileData{
Filename: "goroutines",
Body: b.Bytes(),
}
return fileData, nil
}

View file

@ -5,10 +5,12 @@ package platform
import (
"crypto/ecdsa"
"errors"
"fmt"
"hash/maphash"
"net/http"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
@ -88,6 +90,8 @@ type PlatformService struct {
searchConfigListenerId string
searchLicenseListenerId string
ldapDiagnostic einterfaces.LdapDiagnosticInterface
Jobs *jobs.JobServer
hubs []*Hub
@ -459,6 +463,10 @@ func (ps *PlatformService) initEnterprise() {
ps.SearchEngine.RegisterElasticsearchEngine(elasticsearchInterface(ps))
}
if ldapDiagnosticInterface != nil {
ps.ldapDiagnostic = ldapDiagnosticInterface(ps)
}
if licenseInterface != nil {
ps.licenseManager = licenseInterface(ps)
}
@ -547,6 +555,29 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App
return pluginStatuses, nil
}
func (ps *PlatformService) getPluginManifests() ([]*model.Manifest, error) {
if ps.pluginEnv == nil {
return nil, errors.New("plugin environment not initialized")
}
pluginsEnvironment := ps.pluginEnv.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := pluginsEnvironment.Available()
if err != nil {
return nil, fmt.Errorf("failed to get list of available plugins: %w", err)
}
manifests := make([]*model.Manifest, len(plugins))
for i := range plugins {
manifests[i] = plugins[i].Manifest
}
return manifests, nil
}
func (ps *PlatformService) FileBackend() filestore.FileBackend {
return ps.filestore
}
@ -554,3 +585,17 @@ func (ps *PlatformService) FileBackend() filestore.FileBackend {
func (ps *PlatformService) ExportFileBackend() filestore.FileBackend {
return ps.exportFilestore
}
func (ps *PlatformService) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
return ps.ldapDiagnostic
}
// DatabaseTypeAndSchemaVersion returns the Database type (postgres or mysql) and current version of the schema
func (ps *PlatformService) DatabaseTypeAndSchemaVersion() (string, string, error) {
schemaVersion, err := ps.Store.GetDBSchemaVersion()
if err != nil {
return "", "", err
}
return model.SafeDereference(ps.Config().SqlSettings.DriverName), strconv.Itoa(schemaVersion), nil
}

View file

@ -7,11 +7,13 @@ import (
"math/rand"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -231,3 +233,20 @@ func TestSetTelemetryId(t *testing.T) {
require.Equal(t, clientConfig["DiagnosticId"], id)
})
}
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
th := Setup(t)
defer th.TearDown()
databaseType, schemaVersion, err := th.Service.DatabaseTypeAndSchemaVersion()
require.NoError(t, err)
if *th.Service.Config().SqlSettings.DriverName == model.DatabaseDriverPostgres {
assert.Equal(t, "postgres", databaseType)
} else {
assert.Equal(t, "mysql", databaseType)
}
// It's hard to check wheather the schema version is correct or not.
// So, we just check if it's greater than 1.
assert.GreaterOrEqual(t, schemaVersion, strconv.Itoa(1))
}

View file

@ -0,0 +1,259 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"os"
"runtime"
rpprof "runtime/pprof"
"time"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
const (
envVarInstallType = "MM_INSTALL_TYPE"
unknownDataPoint = "unknown"
)
func (ps *PlatformService) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) ([]model.FileData, error) {
functions := map[string]func(request.CTX) (*model.FileData, error){
"diagnostics": ps.getSupportPacketDiagnostics,
"config": ps.getSanitizedConfigFile,
"cpu profile": ps.getCPUProfile,
"heap profile": ps.getHeapProfile,
"goroutines": ps.getGoroutineProfile,
}
if options != nil && options.IncludeLogs {
functions["mattermost log"] = ps.GetLogFile
functions["notification log"] = ps.GetNotificationLogFile
}
var (
fileDatas []model.FileData
rErr *multierror.Error
)
for name, fn := range functions {
fileData, err := fn(rctx)
if err != nil {
rctx.Logger().Error("Failed to generate file for Support Packet",
mlog.String("file", name),
mlog.Err(err),
)
rErr = multierror.Append(rErr, err)
}
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
}
}
if options != nil && options.IncludeLogs {
advancedLogs, err := ps.GetAdvancedLogs(rctx)
if err != nil {
rctx.Logger().Error("Failed to read advanced log files for Support Packet", mlog.Err(err))
rErr = multierror.Append(rErr, err)
}
for _, log := range advancedLogs {
fileDatas = append(fileDatas, *log)
}
}
return fileDatas, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
d model.SupportPacketDiagnostics
)
d.Version = model.CurrentSupportPacketVersion
/* License */
if license := ps.License(); license != nil {
d.License.Company = license.Customer.Company
d.License.Users = model.SafeDereference(license.Features.Users)
d.License.SkuShortName = license.SkuShortName
d.License.IsTrial = license.IsTrial
d.License.IsGovSKU = license.IsGovSku
}
/* Server */
d.Server.OS = runtime.GOOS
d.Server.Architecture = runtime.GOARCH
d.Server.Hostname, err = os.Hostname()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting hostname"))
}
d.Server.Version = model.CurrentVersion
d.Server.BuildHash = model.BuildHash
installationType := os.Getenv(envVarInstallType)
if installationType == "" {
installationType = unknownDataPoint
}
d.Server.InstallationType = installationType
/* Config */
d.Config.Source = ps.DescribeConfig()
/* DB */
d.Database.Type, d.Database.SchemaVersion, err = ps.DatabaseTypeAndSchemaVersion()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB type and schema version"))
}
databaseVersion, err := ps.Store.GetDbVersion(false)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
} else {
d.Database.Version = databaseVersion
}
d.Database.MasterConnectios = ps.Store.TotalMasterDbConnections()
d.Database.ReplicaConnectios = ps.Store.TotalReadDbConnections()
d.Database.SearchConnections = ps.Store.TotalSearchDbConnections()
/* File store */
d.FileStore.Status = model.StatusOk
err = ps.FileBackend().TestConnection()
if err != nil {
d.FileStore.Status = model.StatusFail
d.FileStore.Error = err.Error()
}
d.FileStore.Driver = ps.FileBackend().DriverName()
/* Websockets */
d.Websocket.Connections = ps.TotalWebsocketConnections()
/* Cluster */
if cluster := ps.Cluster(); cluster != nil {
d.Cluster.ID = cluster.GetClusterId()
clusterInfo := cluster.GetClusterInfos()
d.Cluster.NumberOfNodes = len(clusterInfo)
}
/* LDAP */
if ldap := ps.LdapDiagnostic(); ldap != nil && (*ps.Config().LdapSettings.Enable || *ps.Config().LdapSettings.EnableSync) {
d.LDAP.Status = model.StatusOk
appErr := ldap.RunTest(rctx)
if appErr != nil {
d.LDAP.Status = model.StatusFail
d.LDAP.Error = appErr.Error()
}
severName, serverVersion := unknownDataPoint, unknownDataPoint
// Only if the LDAP test was successful, try to get the LDAP server info
if d.LDAP.Status == model.StatusOk {
severName, serverVersion, err = ldap.GetVendorNameAndVendorVersion(rctx)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
}
if severName == "" {
severName = unknownDataPoint
}
if serverVersion == "" {
serverVersion = unknownDataPoint
}
}
d.LDAP.ServerName = severName
d.LDAP.ServerVersion = serverVersion
}
/* Elastic Search */
if se := ps.SearchEngine.ElasticsearchEngine; se != nil {
d.ElasticSearch.ServerVersion = se.GetFullVersion()
d.ElasticSearch.ServerPlugins = se.GetPlugins()
}
b, err := yaml.Marshal(&d)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
}
fileData := &model.FileData{
Filename: "diagnostics.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) {
config := ps.getSanitizedConfig(rctx)
spConfig := model.SupportPacketConfig{
Config: config,
FeatureFlags: *config.FeatureFlags,
}
sanitizedConfigPrettyJSON, err := json.MarshalIndent(spConfig, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to sanitized config into json")
}
fileData := &model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
}
return fileData, nil
}
func (ps *PlatformService) getCPUProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.StartCPUProfile(&b)
if err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
time.Sleep(cpuProfileDuration)
rpprof.StopCPUProfile()
fileData := &model.FileData{
Filename: "cpu.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getHeapProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("heap").WriteTo(&b, 0)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup heap profile")
}
fileData := &model.FileData{
Filename: "heap.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getGoroutineProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("goroutine").WriteTo(&b, 2)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup goroutine profile")
}
fileData := &model.FileData{
Filename: "goroutines",
Body: b.Bytes(),
}
return fileData, nil
}

View file

@ -0,0 +1,398 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"errors"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"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"
emocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
fmocks "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
)
func TestGenerateSupportPacket(t *testing.T) {
th := Setup(t)
defer th.TearDown()
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() {
err = os.RemoveAll(dir)
assert.NoError(t, err)
})
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.LogSettings.FileLocation = dir
*cfg.NotificationLogSettings.FileLocation = dir
})
logLocation := config.GetLogFileLocation(dir)
notificationsLogLocation := config.GetNotificationsLogFileLocation(dir)
genMockLogFiles := func() {
d1 := []byte("hello\ngo\n")
genErr := os.WriteFile(logLocation, d1, 0600)
require.NoError(t, genErr)
genErr = os.WriteFile(notificationsLogLocation, d1, 0600)
require.NoError(t, genErr)
}
genMockLogFiles()
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
return rFileNames
}
expectedFileNames := []string{
"diagnostics.yaml",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"goroutines",
}
expectedFileNamesWithLogs := append(expectedFileNames, []string{
"mattermost.log",
"notifications.log",
}...)
var fileDatas []model.FileData
t.Run("generate Support Packet with logs", func(t *testing.T) {
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
})
t.Run("generate Support Packet without logs", func(t *testing.T) {
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("remove the log files and ensure that an error is returned", func(t *testing.T) {
err = os.Remove(logLocation)
require.NoError(t, err)
err = os.Remove(notificationsLogLocation)
require.NoError(t, err)
t.Cleanup(genMockLogFiles)
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "failed read mattermost log file")
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("with advanced logs", func(t *testing.T) {
optLDAP := map[string]string{
"filename": path.Join(dir, "ldap.log"),
}
dataLDAP, err := json.Marshal(optLDAP)
require.NoError(t, err)
cfg := mlog.LoggerConfiguration{
"ldap-file": mlog.TargetCfg{
Type: "file",
Format: "json",
Levels: []mlog.Level{
mlog.LvlLDAPError,
mlog.LvlLDAPWarn,
mlog.LvlLDAPInfo,
mlog.LvlLDAPDebug,
},
Options: dataLDAP,
},
}
cfgData, err := json.Marshal(cfg)
require.NoError(t, err)
th.Service.UpdateConfig(func(c *model.Config) {
c.LogSettings.AdvancedLoggingJSON = cfgData
})
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
err = th.Service.Logger().Flush()
require.NoError(t, err)
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
require.NoError(t, err)
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, append(expectedFileNamesWithLogs, "ldap.log"), rFileNames)
found := false
for _, fileData := range fileDatas {
if fileData.Filename == "ldap.log" {
testlib.AssertLog(t, bytes.NewBuffer(fileData.Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
found = true
break
}
}
assert.True(t, found)
})
}
func TestGetSupportPacketDiagnostics(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Setenv(envVarInstallType, "docker")
licenseUsers := 100
license := model.NewTestLicense("ldap")
license.SkuShortName = model.LicenseShortSkuEnterprise
license.Features.Users = model.NewPointer(licenseUsers)
ok := th.Service.SetLicense(license)
require.True(t, ok)
getDiagnostics := func(t *testing.T) *model.SupportPacketDiagnostics {
t.Helper()
fileData, err := th.Service.getSupportPacketDiagnostics(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "diagnostics.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var d model.SupportPacketDiagnostics
require.NoError(t, yaml.Unmarshal(fileData.Body, &d))
return &d
}
t.Run("Happy path", func(t *testing.T) {
d := getDiagnostics(t)
assert.Equal(t, 1, d.Version)
/* License */
assert.Equal(t, "My awesome Company", d.License.Company)
assert.Equal(t, licenseUsers, d.License.Users)
assert.Equal(t, model.LicenseShortSkuEnterprise, d.License.SkuShortName)
assert.Equal(t, false, d.License.IsTrial)
assert.Equal(t, false, d.License.IsGovSKU)
/* Server information */
assert.NotEmpty(t, d.Server.OS)
assert.NotEmpty(t, d.Server.Architecture)
assert.NotEmpty(t, d.Server.Hostname)
assert.Equal(t, model.CurrentVersion, d.Server.Version)
// BuildHash is not present in tests
assert.Equal(t, "docker", d.Server.InstallationType)
/* Config */
assert.Equal(t, "memory://", d.Config.Source)
/* DB */
assert.NotEmpty(t, d.Database.Type)
assert.NotEmpty(t, d.Database.Version)
assert.NotEmpty(t, d.Database.SchemaVersion)
assert.NotZero(t, d.Database.MasterConnectios)
assert.Zero(t, d.Database.ReplicaConnectios)
assert.Zero(t, d.Database.SearchConnections)
/* File store */
assert.Equal(t, "OK", d.FileStore.Status)
assert.Empty(t, d.FileStore.Error)
assert.Equal(t, "local", d.FileStore.Driver)
/* Websockets */
assert.Zero(t, d.Websocket.Connections)
/* Cluster */
assert.Empty(t, d.Cluster.ID)
assert.Zero(t, d.Cluster.NumberOfNodes)
/* LDAP */
assert.Empty(t, d.LDAP.Status)
assert.Empty(t, d.LDAP.Error)
assert.Empty(t, d.LDAP.ServerName)
assert.Empty(t, d.LDAP.ServerVersion)
/* Elastic Search */
assert.Empty(t, d.ElasticSearch.ServerVersion)
assert.Empty(t, d.ElasticSearch.ServerPlugins)
})
t.Run("filestore fails", func(t *testing.T) {
fb := &fmocks.FileBackend{}
err := SetFileStore(fb)(th.Service)
require.NoError(t, err)
fb.On("DriverName").Return("mock")
fb.On("TestConnection").Return(errors.New("all broken"))
packet := getDiagnostics(t)
assert.Equal(t, "FAIL", packet.FileStore.Status)
assert.Equal(t, "all broken", packet.FileStore.Error)
assert.Equal(t, "mock", packet.FileStore.Driver)
})
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "", packet.LDAP.ServerName)
assert.Equal(t, "", packet.LDAP.ServerVersion)
})
th.Service.UpdateConfig(func(cfg *model.Config) {
cfg.LdapSettings.EnableSync = model.NewPointer(true)
})
t.Run("no LDAP vendor info found", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("", "", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(nil)
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "OK", packet.LDAP.Status)
assert.Empty(t, packet.LDAP.Error)
assert.Equal(t, "unknown", packet.LDAP.ServerName)
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
})
t.Run("found LDAP vendor info", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("some vendor", "v1.0.0", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(nil)
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "OK", packet.LDAP.Status)
assert.Empty(t, packet.LDAP.Error)
assert.Equal(t, "some vendor", packet.LDAP.ServerName)
assert.Equal(t, "v1.0.0", packet.LDAP.ServerVersion)
})
t.Run("LDAP test fails", func(t *testing.T) {
ldapMock := &emocks.LdapDiagnosticInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("some vendor", "v1.0.0", nil)
ldapMock.On(
"RunTest",
mock.AnythingOfType("*request.Context"),
).Return(model.NewAppError("", "some error", nil, "", 0))
th.Service.ldapDiagnostic = ldapMock
packet := getDiagnostics(t)
assert.Equal(t, "FAIL", packet.LDAP.Status)
assert.Equal(t, "some error", packet.LDAP.Error)
assert.Equal(t, "unknown", packet.LDAP.ServerName)
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
})
}
func TestGetSanitizedConfigFile(t *testing.T) {
t.Setenv("MM_FEATUREFLAGS_TestFeature", "true")
th := Setup(t)
defer th.TearDown()
th.Service.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewPointer("example.com")
})
// Happy path where we have a sanitized config file with no err
fileData, err := th.Service.getSanitizedConfigFile(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "sanitized_config.json", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var config model.Config
err = json.Unmarshal(fileData.Body, &config)
require.NoError(t, err)
// Ensure sensitive fields are redacted
assert.Equal(t, model.FakeSetting, *config.SqlSettings.DataSource)
// Ensure non-sensitive fields are present
assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections)
// Ensure feature flags are present
assert.Equal(t, "true", config.FeatureFlags.TestFeature)
}
func TestGetCPUProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getCPUProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "cpu.prof", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetHeapProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getHeapProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "heap.prof", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}
func TestGetGoroutineProfile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
fileData, err := th.Service.getGoroutineProfile(th.Context)
require.NoError(t, err)
assert.Equal(t, "goroutines", fileData.Filename)
assert.Positive(t, len(fileData.Body))
}

View file

@ -90,6 +90,20 @@ func (a *App) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
return roles, nil
}
func (a *App) DeleteRole(id string) (*model.Role, *model.AppError) {
role, err := a.Srv().Store().Role().Delete(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteRole", "app.role.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteRole", "app.role.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return role, nil
}
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
// moderated, and the value of the permission on the higher-scoped scheme.
func (s *Server) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {

View file

@ -14,7 +14,6 @@ import (
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"syscall"
@ -592,12 +591,6 @@ func (s *Server) Channels() *Channels {
return s.ch
}
// Return Database type (postgres or mysql) and current version of the schema
func (s *Server) DatabaseTypeAndSchemaVersion() (string, string) {
schemaVersion, _ := s.Store().GetDBSchemaVersion()
return *s.platform.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion)
}
func (s *Server) startInterClusterServices(license *model.License) error {
if license == nil {
mlog.Debug("No license provided; Remote Cluster services disabled")

View file

@ -164,34 +164,6 @@ func TestStartServerTLSSuccess(t *testing.T) {
require.NoError(t, serverErr)
}
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
sqlDrivernameEnvironment := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if sqlDrivernameEnvironment != "" {
defer os.Setenv("MM_SQLSETTINGS_DRIVERNAME", sqlDrivernameEnvironment)
} else {
defer os.Unsetenv("MM_SQLSETTINGS_DRIVERNAME")
}
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "postgres")
th := Setup(t)
defer th.TearDown()
databaseType, mattermostVersion := th.Server.DatabaseTypeAndSchemaVersion()
assert.Equal(t, "postgres", databaseType)
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "mysql")
th2 := Setup(t)
defer th2.TearDown()
databaseType, mattermostVersion = th2.Server.DatabaseTypeAndSchemaVersion()
assert.Equal(t, "mysql", databaseType)
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
}
func TestStartServerTLSVersion(t *testing.T) {
configStore, _ := config.NewMemoryStore()
store, _ := config.NewStoreFromBacking(configStore, nil, false)

View file

@ -5,13 +5,12 @@ package app
import (
"encoding/json"
"runtime"
"slices"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
@ -19,39 +18,36 @@ import (
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
// A array of the functions that we can iterate through since they all have the same return value
func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData {
functions := map[string]func(c request.CTX) (*model.FileData, error){
"support packet": a.generateSupportPacketYaml,
"plugins": a.createPluginsFile,
"config": a.createSanitizedConfigFile,
"cpu profile": a.Srv().Platform().CreateCPUProfile,
"heap profile": a.Srv().Platform().CreateHeapProfile,
"goroutines": a.Srv().Platform().CreateGoroutineProfile,
"metadata": a.createSupportPacketMetadata,
"metadata": a.getSupportPacketMetadata,
"stats": a.getSupportPacketStats,
"jobs": a.getSupportPacketJobList,
"permissions": a.getSupportPacketPermissionsInfo,
"plugins": a.getPluginsFile,
}
if options.IncludeLogs {
functions["mattermost log"] = a.Srv().Platform().GetLogFile
functions["notification log"] = a.Srv().Platform().GetNotificationLogFile
}
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
var warnings *multierror.Error
// Creating an array of files that we are going to be adding to our zip file
var fileDatas []model.FileData
var wg sync.WaitGroup
var mut sync.Mutex // Protects warnings and fileDatas
var (
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
warnings *multierror.Error
// Creating an array of files that we are going to be adding to our zip file
fileDatas []model.FileData
wg sync.WaitGroup
mut sync.Mutex // Protects warnings and fileDatas
)
wg.Add(1)
go func() {
defer wg.Done()
for name, fn := range functions {
fileData, err := fn(c)
fileData, err := fn(rctx)
mut.Lock()
if err != nil {
c.Logger().Error("Failed to generate file for Support Packet", mlog.String("file", name), mlog.Err(err))
rctx.Logger().Error("Failed to generate file for Support Packet",
mlog.String("file", name),
mlog.Err(err),
)
warnings = multierror.Append(warnings, err)
}
@ -60,6 +56,17 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
}
mut.Unlock()
}
// Generate platform support packet
files, err := a.Srv().Platform().GenerateSupportPacket(rctx, options)
mut.Lock()
if err != nil {
warnings = multierror.Append(warnings, err)
}
if files != nil {
fileDatas = append(fileDatas, files...)
}
mut.Unlock()
}()
// Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time
@ -68,10 +75,10 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
go func() {
defer wg.Done()
files, err := cluster.GenerateSupportPacket(c, options)
files, err := cluster.GenerateSupportPacket(rctx, options)
mut.Lock()
if err != nil {
c.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
rctx.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
warnings = multierror.Append(warnings, err)
}
@ -84,7 +91,7 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
wg.Wait()
pluginContext := pluginContext(c)
pluginContext := pluginContext(rctx)
a.ch.RunMultiHook(func(hooks plugin.Hooks, manifest *model.Manifest) bool {
// If the plugin defined the support_packet prop it means there is a UI element to include it in the support packet.
// Check if the plugin is in the list of plugins to include in the Support Packet.
@ -97,7 +104,7 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
// Otherwise, just call the hook as the plugin decided to always include it in the Support Packet.
pluginData, err := hooks.GenerateSupportData(pluginContext)
if err != nil {
c.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.String("plugin", manifest.Id), mlog.Err(err))
rctx.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.String("plugin", manifest.Id), mlog.Err(err))
warnings = multierror.Append(warnings, err)
return true
}
@ -120,230 +127,212 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
return fileDatas
}
func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) {
var rErr *multierror.Error
/* DB */
databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion()
databaseVersion, err := a.Srv().Store().GetDbVersion(false)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
}
/* Cluster */
var clusterID string
if cluster := a.Cluster(); cluster != nil {
clusterID = cluster.GetClusterId()
}
/* File store */
fileDriver := a.Srv().Platform().FileBackend().DriverName()
fileStatus := model.StatusOk
err = a.Srv().Platform().FileBackend().TestConnection()
if err != nil {
fileStatus = model.StatusFail + ": " + err.Error()
}
/* LDAP */
var vendorName, vendorVersion string
if ldap := a.Ldap(); ldap != nil && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
vendorName, vendorVersion, err = ldap.GetVendorNameAndVendorVersion(c)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
}
if vendorName == "" {
vendorName = "unknown"
}
if vendorVersion == "" {
vendorVersion = "unknown"
}
}
/* Elastic Search */
var elasticServerVersion string
var elasticServerPlugins []string
if se := a.Srv().Platform().SearchEngine.ElasticsearchEngine; se != nil {
elasticServerVersion = se.GetFullVersion()
elasticServerPlugins = se.GetPlugins()
}
/* License */
func (a *App) getSupportPacketStats(rctx request.CTX) (*model.FileData, error) {
var (
licenseTo string
supportedUsers int
isTrial bool
rErr *multierror.Error
err error
stats model.SupportPacketStats
)
if license := a.Srv().License(); license != nil {
licenseTo = license.Customer.Company
supportedUsers = *license.Features.Users
isTrial = license.IsTrial
}
/* Server stats */
uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
stats.RegisteredUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting user count"))
rErr = multierror.Append(errors.Wrap(err, "failed to get registered user count"))
}
var (
totalChannels int
totalPosts int
totalTeams int
websocketConnections int
masterDbConnections int
replicaDbConnections int
dailyActiveUsers int
monthlyActiveUsers int
inactiveUserCount int
)
analytics, appErr := a.GetAnalyticsForSupportPacket(c)
if appErr != nil {
rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics"))
} else {
if len(analytics) < 11 {
rErr = multierror.Append(errors.New("not enough analytics information found"))
} else {
totalChannels = int(analytics[0].Value) + int(analytics[1].Value)
totalPosts = int(analytics[2].Value)
totalTeams = int(analytics[4].Value)
websocketConnections = int(analytics[5].Value)
masterDbConnections = int(analytics[6].Value)
replicaDbConnections = int(analytics[7].Value)
dailyActiveUsers = int(analytics[8].Value)
monthlyActiveUsers = int(analytics[9].Value)
inactiveUserCount = int(analytics[10].Value)
}
}
/* Jobs */
dataRetentionJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeDataRetention, 0, 2)
stats.ActiveUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get active user count"))
}
messageExportJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMessageExport, 0, 2)
stats.DailyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get daily active user count"))
}
elasticPostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostIndexing, 0, 2)
stats.MonthlyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get monthly active user count"))
}
elasticPostAggregationJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostAggregation, 0, 2)
stats.DeactivatedUsers, err = a.Srv().Store().User().AnalyticsGetInactiveUsersCount()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get deactivated user count"))
}
blevePostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeBlevePostIndexing, 0, 2)
stats.Guests, err = a.Srv().Store().User().AnalyticsGetGuestCount()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get guest count"))
}
ldapSyncJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeLdapSync, 0, 2)
stats.BotAccounts, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeBotAccounts: true, ExcludeRegularUsers: true})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get bot acount count"))
}
migrationJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMigrations, 0, 2)
stats.Posts, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
rErr = multierror.Append(errors.Wrap(err, "failed to get post count"))
}
// Creating the struct for Support Packet yaml file
supportPacket := model.SupportPacket{
/* Build information */
ServerOS: runtime.GOOS,
ServerArchitecture: runtime.GOARCH,
ServerVersion: model.CurrentVersion,
BuildHash: model.BuildHash,
openChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypeOpen)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get open channels count"))
}
privateChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypePrivate)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get private channels count"))
}
stats.Channels = openChannels + privateChannels
/* DB */
DatabaseType: databaseType,
DatabaseVersion: databaseVersion,
DatabaseSchemaVersion: databaseSchemaVersion,
WebsocketConnections: websocketConnections,
MasterDbConnections: masterDbConnections,
ReplicaDbConnections: replicaDbConnections,
/* Cluster */
ClusterID: clusterID,
/* File store */
FileDriver: fileDriver,
FileStatus: fileStatus,
/* LDAP */
LdapVendorName: vendorName,
LdapVendorVersion: vendorVersion,
/* Elastic Search */
ElasticServerVersion: elasticServerVersion,
ElasticServerPlugins: elasticServerPlugins,
/* License */
LicenseTo: licenseTo,
LicenseSupportedUsers: supportedUsers,
LicenseIsTrial: isTrial,
/* Server stats */
ActiveUsers: int(uniqueUserCount),
DailyActiveUsers: dailyActiveUsers,
MonthlyActiveUsers: monthlyActiveUsers,
InactiveUserCount: inactiveUserCount,
TotalPosts: totalPosts,
TotalChannels: totalChannels,
TotalTeams: totalTeams,
/* Jobs */
DataRetentionJobs: dataRetentionJobs,
MessageExportJobs: messageExportJobs,
ElasticPostIndexingJobs: elasticPostIndexingJobs,
ElasticPostAggregationJobs: elasticPostAggregationJobs,
BlevePostIndexingJobs: blevePostIndexingJobs,
LdapSyncJobs: ldapSyncJobs,
MigrationJobs: migrationJobs,
stats.Teams, err = a.Srv().Store().Team().AnalyticsTeamCount(nil)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get team count"))
}
// Marshal to a YAML File
supportPacketYaml, err := yaml.Marshal(&supportPacket)
stats.SlashCommands, err = a.Srv().Store().Command().AnalyticsCommandCount("")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get command count"))
}
stats.IncomingWebhooks, err = a.Srv().Store().Webhook().AnalyticsIncomingCount("", "")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get incoming webhook count"))
}
stats.OutgoingWebhooks, err = a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get outgoing webhook count"))
}
b, err := yaml.Marshal(&stats)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
}
fileData := &model.FileData{
Filename: "support_packet.yaml",
Body: supportPacketYaml,
Filename: "stats.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) {
// Getting sanitized config, prettifying it, and then adding it to our file data array
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
func (a *App) getSupportPacketJobList(rctx request.CTX) (*model.FileData, error) {
const numberOfJobsRuns = 5
var (
rErr *multierror.Error
err error
jobs model.SupportPacketJobList
)
jobs.LDAPSyncJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeLdapSync, 0, numberOfJobsRuns)
if err != nil {
return nil, errors.Wrap(err, "failed to sanitized config into json")
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs"))
}
jobs.DataRetentionJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeDataRetention, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs"))
}
jobs.MessageExportJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMessageExport, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs"))
}
jobs.ElasticPostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostIndexing, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs"))
}
jobs.ElasticPostAggregationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostAggregation, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs"))
}
jobs.BlevePostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeBlevePostIndexing, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs"))
}
jobs.MigrationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMigrations, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
}
b, err := yaml.Marshal(&jobs)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal jobs list into yaml"))
}
fileData := &model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
Filename: "jobs.yaml",
Body: b,
}
return fileData, nil
return fileData, rErr.ErrorOrNil()
}
func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) {
func (a *App) getSupportPacketPermissionsInfo(_ request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
permissions model.SupportPacketPermissionInfo
)
var allSchemes []*model.Scheme
perPage := 100
page := 0
for {
schemes, appErr := a.GetSchemesPage("", page, perPage)
if appErr != nil {
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of schemes"))
break
}
allSchemes = append(allSchemes, schemes...)
if len(schemes) < perPage {
break
}
page++
}
for _, s := range allSchemes {
s.Sanitize()
}
permissions.Schemes = allSchemes
roles, appErr := a.GetAllRoles()
if appErr != nil {
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of roles"))
}
for _, r := range roles {
r.Sanitize()
}
permissions.Roles = roles
b, err := yaml.Marshal(&permissions)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal permission info into yaml"))
}
fileData := &model.FileData{
Filename: "permissions.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (a *App) getPluginsFile(_ request.CTX) (*model.FileData, error) {
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
pluginsResponse, appErr := a.GetPlugins()
plugins, appErr := a.GetPlugins()
if appErr != nil {
return nil, errors.Wrap(appErr, "failed to get plugin list for Support Packet")
}
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
var pluginList model.SupportPacketPluginList
for _, p := range plugins.Active {
pluginList.Enabled = append(pluginList.Enabled, p.Manifest)
}
for _, p := range plugins.Inactive {
pluginList.Disabled = append(pluginList.Disabled, p.Manifest)
}
pluginsPrettyJSON, err := json.MarshalIndent(pluginList, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal plugin list into json")
}
@ -355,7 +344,7 @@ func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) {
return fileData, nil
}
func (a *App) createSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
func (a *App) getSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil)
if err != nil {
return nil, errors.Wrap(err, "failed to generate Packet metadata")

View file

@ -4,200 +4,29 @@
package app
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
smocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/mattermost/mattermost/server/v8/config"
emocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
fmocks "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
)
func TestCreatePluginsFile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Happy path where we have a plugins file with no err
fileData, err := th.App.createPluginsFile(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "plugins.json", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
// Turn off plugins so we can get an error
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = false
})
// Plugins off in settings so no fileData and we get a warning instead
fileData, err = th.App.createPluginsFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "failed to get plugin list for Support Packet")
}
func TestGenerateSupportPacketYaml(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
licenseUsers := 100
license := model.NewTestLicense("ldap")
license.Features.Users = model.NewPointer(licenseUsers)
th.App.Srv().SetLicense(license)
generateSupportPacket := func(t *testing.T) *model.SupportPacket {
t.Helper()
fileData, err := th.App.generateSupportPacketYaml(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "support_packet.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var packet model.SupportPacket
require.NoError(t, yaml.Unmarshal(fileData.Body, &packet))
require.NotNil(t, packet)
return &packet
}
t.Run("Happy path", func(t *testing.T) {
// Happy path where we have a Support Packet yaml file without any warnings
packet := generateSupportPacket(t)
/* Build information */
assert.NotEmpty(t, packet.ServerOS)
assert.NotEmpty(t, packet.ServerArchitecture)
assert.Equal(t, model.CurrentVersion, packet.ServerVersion)
// BuildHash is not present in tests
/* DB */
assert.NotEmpty(t, packet.DatabaseType)
assert.NotEmpty(t, packet.DatabaseVersion)
assert.NotEmpty(t, packet.DatabaseSchemaVersion)
assert.Zero(t, packet.WebsocketConnections)
assert.NotZero(t, packet.MasterDbConnections)
assert.Zero(t, packet.ReplicaDbConnections)
/* Cluster */
assert.Empty(t, packet.ClusterID)
/* File store */
assert.Equal(t, "local", packet.FileDriver)
assert.Equal(t, "OK", packet.FileStatus)
/* LDAP */
assert.Empty(t, packet.LdapVendorName)
assert.Empty(t, packet.LdapVendorVersion)
/* Elastic Search */
assert.Empty(t, packet.ElasticServerVersion)
assert.Empty(t, packet.ElasticServerPlugins)
/* License */
assert.Equal(t, "My awesome Company", packet.LicenseTo)
assert.Equal(t, licenseUsers, packet.LicenseSupportedUsers)
assert.Equal(t, false, packet.LicenseIsTrial)
/* Server stats */
assert.Equal(t, 3, packet.ActiveUsers) // from InitBasic()
assert.Equal(t, 0, packet.DailyActiveUsers)
assert.Equal(t, 0, packet.MonthlyActiveUsers)
assert.Equal(t, 0, packet.InactiveUserCount)
assert.Equal(t, 5, packet.TotalPosts) // from InitBasic()
assert.Equal(t, 3, packet.TotalChannels) // from InitBasic()
assert.Equal(t, 1, packet.TotalTeams) // from InitBasic()
/* Jobs */
assert.Empty(t, packet.DataRetentionJobs)
assert.Empty(t, packet.MessageExportJobs)
assert.Empty(t, packet.ElasticPostIndexingJobs)
assert.Empty(t, packet.ElasticPostAggregationJobs)
assert.Empty(t, packet.BlevePostIndexingJobs)
assert.Empty(t, packet.LdapSyncJobs)
assert.Empty(t, packet.MigrationJobs)
})
t.Run("post count should be present if number of users extends AnalyticsSettings.MaxUsersForStatistics", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AnalyticsSettings.MaxUsersForStatistics = model.NewPointer(1)
})
for i := 0; i < 5; i++ {
p := th.CreatePost(th.BasicChannel)
require.NotNil(t, p)
}
// InitBasic() already creats 5 posts
packet := generateSupportPacket(t)
assert.Equal(t, 10, packet.TotalPosts)
})
t.Run("filestore fails", func(t *testing.T) {
fb := &fmocks.FileBackend{}
platform.SetFileStore(fb)(th.Server.Platform())
fb.On("DriverName").Return("mock")
fb.On("TestConnection").Return(errors.New("all broken"))
packet := generateSupportPacket(t)
assert.Equal(t, "mock", packet.FileDriver)
assert.Equal(t, "FAIL: all broken", packet.FileStatus)
})
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
ldapMock := &emocks.LdapInterface{}
th.App.Channels().Ldap = ldapMock
packet := generateSupportPacket(t)
assert.Equal(t, "", packet.LdapVendorName)
assert.Equal(t, "", packet.LdapVendorVersion)
})
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.LdapSettings.EnableSync = model.NewPointer(true)
})
t.Run("no LDAP vendor info found", func(t *testing.T) {
ldapMock := &emocks.LdapInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("", "", nil)
th.App.Channels().Ldap = ldapMock
packet := generateSupportPacket(t)
assert.Equal(t, "unknown", packet.LdapVendorName)
assert.Equal(t, "unknown", packet.LdapVendorVersion)
})
t.Run("found LDAP vendor info", func(t *testing.T) {
ldapMock := &emocks.LdapInterface{}
ldapMock.On(
"GetVendorNameAndVendorVersion",
mock.AnythingOfType("*request.Context"),
).Return("some vendor", "v1.0.0", nil)
th.App.Channels().Ldap = ldapMock
packet := generateSupportPacket(t)
assert.Equal(t, "some vendor", packet.LdapVendorName)
assert.Equal(t, "v1.0.0", packet.LdapVendorVersion)
})
}
func TestGenerateSupportPacket(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.SetPhase2PermissionsMigrationStatus(true)
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() {
@ -222,29 +51,42 @@ func TestGenerateSupportPacket(t *testing.T) {
}
genMockLogFiles()
t.Run("generate Support Packet with logs", func(t *testing.T) {
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
var rFileNames []string
testFiles := []string{
"support_packet.yaml",
"metadata.yaml",
"plugins.json",
"sanitized_config.json",
"mattermost.log",
"notifications.log",
"cpu.prof",
"heap.prof",
"goroutines",
}
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
return rFileNames
}
expectedFileNames := []string{
"metadata.yaml",
"stats.yaml",
"jobs.yaml",
"permissions.yaml",
"plugins.json",
"sanitized_config.json",
"diagnostics.yaml",
"cpu.prof",
"heap.prof",
"goroutines",
}
expectedFileNamesWithLogs := append(expectedFileNames, []string{
"mattermost.log",
"notifications.log",
}...)
t.Run("generate Support Packet with logs", func(t *testing.T) {
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
})
t.Run("generate Support Packet without logs", func(t *testing.T) {
@ -252,27 +94,12 @@ func TestGenerateSupportPacket(t *testing.T) {
IncludeLogs: false,
})
testFiles := []string{
"support_packet.yaml",
"metadata.yaml",
"plugins.json",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"goroutines",
}
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames := getFileNames(t, fileDatas)
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("remove the log files and ensure that warning.txt file is generated", func(t *testing.T) {
// Remove these two files and ensure that warning.txt file is generated
err = os.Remove(logLocation)
require.NoError(t, err)
err = os.Remove(notificationsLogLocation)
@ -282,24 +109,9 @@ func TestGenerateSupportPacket(t *testing.T) {
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
testFiles := []string{
"support_packet.yaml",
"metadata.yaml",
"plugins.json",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"warning.txt",
"goroutines",
}
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames := getFileNames(t, fileDatas)
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
assert.ElementsMatch(t, append(expectedFileNames, "warning.txt"), rFileNames)
})
t.Run("steps that generated an error should still return file data", func(t *testing.T) {
@ -320,45 +132,536 @@ func TestGenerateSupportPacket(t *testing.T) {
mockStore.On("Webhook").Return(th.App.Srv().Store().Webhook())
mockStore.On("System").Return(th.App.Srv().Store().System())
mockStore.On("License").Return(th.App.Srv().Store().License())
mockStore.On("Command").Return(th.App.Srv().Store().Command())
mockStore.On("Role").Return(th.App.Srv().Store().Role())
mockStore.On("Scheme").Return(th.App.Srv().Store().Scheme())
mockStore.On("Close").Return(nil)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
mockStore.On("GetDbVersion", false).Return("1.0.0", nil)
mockStore.On("TotalMasterDbConnections").Return(30)
mockStore.On("TotalReadDbConnections").Return(20)
mockStore.On("TotalSearchDbConnections").Return(10)
oldStore := th.App.Srv().Store()
t.Cleanup(func() {
th.App.Srv().SetStore(oldStore)
})
th.App.Srv().SetStore(&mockStore)
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
rFileNames := getFileNames(t, fileDatas)
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
assert.Contains(t, rFileNames, "warning.txt")
assert.Contains(t, rFileNames, "support_packet.yaml")
assert.Contains(t, rFileNames, "stats.yaml")
assert.ElementsMatch(t, append(expectedFileNames, "warning.txt"), rFileNames)
})
pluginID := "testplugin"
pluginCode := `
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type TestPlugin struct {
plugin.MattermostPlugin
}
func (p *TestPlugin) GenerateSupportData(c *plugin.Context) ([]*model.FileData, error) {
return []*model.FileData{{
Filename: "testplugin/diagnostics.yaml",
Body: []byte("foo"),
}}, nil
}
func main() {
plugin.ClientMain(&TestPlugin{})
}`
t.Run("Support Packet always contains plugin data if the plugin doesn't define the support_packet prop", func(t *testing.T) {
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
t.Cleanup(func() {
appErr := th.App.ch.RemovePlugin(pluginID)
require.Nil(t, appErr)
})
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, append(expectedFileNames, "testplugin/diagnostics.yaml"), rFileNames)
})
t.Run("Support Packet contains plugin data if the plugin defines the support_packet prop and it gets queried", func(t *testing.T) {
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "props": {"support_packet": "some text"}}`
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
t.Cleanup(func() {
appErr := th.App.ch.RemovePlugin(pluginID)
require.Nil(t, appErr)
})
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
PluginPackets: []string{pluginID},
})
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, append(expectedFileNames, "testplugin/diagnostics.yaml"), rFileNames)
})
t.Run("Support Packet doesn't contain plugin data if the plugin defines the support_packet prop and it doesn't get queried", func(t *testing.T) {
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "props": {"support_packet": "some text"}}`
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
t.Cleanup(func() {
appErr := th.App.ch.RemovePlugin(pluginID)
require.Nil(t, appErr)
})
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
rFileNames := getFileNames(t, fileDatas)
assert.ElementsMatch(t, expectedFileNames, rFileNames)
})
t.Run("Plugin config values in the Support Packet are obfuscated, if the plugin marks them as secrets", func(t *testing.T) {
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}, "settings_schema": {"settings": [{"key": "foo", "type": "text"}, {"key": "bar", "type": "text", "secret": true}]}}`
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
t.Cleanup(func() {
appErr := th.App.ch.RemovePlugin(pluginID)
require.Nil(t, appErr)
})
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins[pluginID] = map[string]any{
"foo": "foo_value",
"bar": "bar_value",
}
})
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
found := false
for _, f := range fileDatas {
if f.Filename != "sanitized_config.json" {
continue
}
var config model.Config
err = json.Unmarshal(f.Body, &config)
require.NoError(t, err)
assert.Equal(t, "foo_value", config.PluginSettings.Plugins[pluginID]["foo"])
assert.Equal(t, model.FakeSetting, config.PluginSettings.Plugins[pluginID]["bar"])
found = true
}
assert.True(t, found)
})
}
func TestCreateSanitizedConfigFile(t *testing.T) {
func TestGetPluginsFile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Happy path where we have a sanitized config file with no err
fileData, err := th.App.createSanitizedConfigFile(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "sanitized_config.json", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
getJobList := func(t *testing.T) *model.SupportPacketPluginList {
t.Helper()
fileData, err := th.App.getPluginsFile(th.Context)
assert.NoError(t, err)
require.NotNil(t, fileData)
assert.Equal(t, "plugins.json", fileData.Filename)
assert.Positive(t, len(fileData.Body))
var pl model.SupportPacketPluginList
err = json.Unmarshal(fileData.Body, &pl)
require.NoError(t, err)
return &pl
}
t.Run("no errors if no plugins are installed", func(t *testing.T) {
pl := getJobList(t)
assert.Len(t, pl.Enabled, 0)
assert.Len(t, pl.Disabled, 0)
})
t.Run("two plugins are installed", func(t *testing.T) {
path, _ := fileutils.FindDir("tests")
bundle1, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
manifest1, appErr := th.App.InstallPlugin(bytes.NewReader(bundle1), false)
require.Nil(t, appErr)
require.Equal(t, "testplugin", manifest1.Id)
appErr = th.App.EnablePlugin(manifest1.Id)
require.Nil(t, appErr)
bundle2, err := os.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
require.NoError(t, err)
manifest2, appErr := th.App.InstallPlugin(bytes.NewReader(bundle2), false)
require.Nil(t, appErr)
require.Equal(t, "testplugin2", manifest2.Id)
pl := getJobList(t)
require.Len(t, pl.Enabled, 1)
assert.Equal(t, "testplugin", pl.Enabled[0].Id)
require.Len(t, pl.Disabled, 1)
assert.Equal(t, "testplugin2", pl.Disabled[0].Id)
})
t.Run("error if plugin are disabled", func(t *testing.T) {
// Turn off plugins so we can get an error
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = false
})
// Plugins off in settings so no fileData and we get a warning instead
fileData, err := th.App.getPluginsFile(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "failed to get plugin list for Support Packet")
})
}
func TestCreateSupportPacketMetadata(t *testing.T) {
func TestGetSupportPacketStats(t *testing.T) {
th := Setup(t)
generateStats := func(t *testing.T) *model.SupportPacketStats {
t.Helper()
fileData, err := th.App.getSupportPacketStats(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "stats.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var packet model.SupportPacketStats
err = yaml.Unmarshal(fileData.Body, &packet)
require.NoError(t, err)
return &packet
}
t.Run("fresh server", func(t *testing.T) {
sp := generateStats(t)
assert.Equal(t, int64(0), sp.RegisteredUsers)
assert.Equal(t, int64(0), sp.ActiveUsers)
assert.Equal(t, int64(0), sp.DailyActiveUsers)
assert.Equal(t, int64(0), sp.MonthlyActiveUsers)
assert.Equal(t, int64(0), sp.DeactivatedUsers)
assert.Equal(t, int64(0), sp.Guests)
assert.Equal(t, int64(0), sp.BotAccounts)
assert.Equal(t, int64(0), sp.Posts)
assert.Equal(t, int64(0), sp.Channels)
assert.Equal(t, int64(0), sp.Teams)
assert.Equal(t, int64(0), sp.SlashCommands)
assert.Equal(t, int64(0), sp.IncomingWebhooks)
assert.Equal(t, int64(0), sp.OutgoingWebhooks)
})
t.Run("Happy path", func(t *testing.T) {
var user *model.User
for i := 0; i < 4; i++ {
user = th.CreateUser()
}
th.BasicUser = user
for i := 0; i < 3; i++ {
deactivatedUser := th.CreateUser()
require.NotNil(t, deactivatedUser)
_, appErr := th.App.UpdateActive(th.Context, deactivatedUser, false)
require.Nil(t, appErr)
}
for i := 0; i < 2; i++ {
guest := th.CreateGuest()
require.NotNil(t, guest)
}
th.CreateBot()
team := th.CreateTeam()
channel := th.CreateChannel(th.Context, team)
for i := 0; i < 3; i++ {
p := th.CreatePost(channel)
require.NotNil(t, p)
}
cmd, appErr := th.App.CreateCommand(&model.Command{
CreatorId: user.Id,
TeamId: team.Id,
Trigger: "test",
Method: model.CommandMethodGet,
URL: "http://nowhere.com/",
})
require.Nil(t, appErr)
require.NotNil(t, cmd)
webhookIn, appErr := th.App.CreateIncomingWebhookForChannel(user.Id, channel, &model.IncomingWebhook{ChannelId: channel.Id})
require.Nil(t, appErr)
require.NotNil(t, webhookIn)
webhookOut, appErr := th.App.CreateOutgoingWebhook(&model.OutgoingWebhook{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: th.BasicUser.Id,
CallbackURLs: []string{"http://nowhere.com/"},
})
require.Nil(t, appErr)
require.NotNil(t, webhookOut)
sp := generateStats(t)
assert.Equal(t, int64(9), sp.RegisteredUsers)
assert.Equal(t, int64(6), sp.ActiveUsers)
assert.Equal(t, int64(0), sp.DailyActiveUsers)
assert.Equal(t, int64(0), sp.MonthlyActiveUsers)
assert.Equal(t, int64(3), sp.DeactivatedUsers)
assert.Equal(t, int64(2), sp.Guests)
assert.Equal(t, int64(1), sp.BotAccounts)
assert.Equal(t, int64(4), sp.Posts) // 1 from the bot creation and 3 created directly
assert.Equal(t, int64(3), sp.Channels) // 2 from the team creation and 1 created directly
assert.Equal(t, int64(1), sp.Teams)
assert.Equal(t, int64(1), sp.SlashCommands)
assert.Equal(t, int64(1), sp.IncomingWebhooks)
assert.Equal(t, int64(1), sp.OutgoingWebhooks)
})
// Reset test server
th.TearDown()
th = Setup(t).InitBasic()
defer th.TearDown()
t.Run("post count should be present if number of users extends AnalyticsSettings.MaxUsersForStatistics", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.AnalyticsSettings.MaxUsersForStatistics = model.NewPointer(1)
})
for i := 0; i < 5; i++ {
p := th.CreatePost(th.BasicChannel)
require.NotNil(t, p)
}
// InitBasic() already creats 5 posts
packet := generateStats(t)
assert.Equal(t, int64(10), packet.Posts)
})
}
func TestGetSupportPacketJobList(t *testing.T) {
th := Setup(t)
defer th.TearDown()
getJobList := func(t *testing.T) *model.SupportPacketJobList {
t.Helper()
fileData, err := th.App.getSupportPacketJobList(th.Context)
require.NoError(t, err)
require.NotNil(t, fileData)
assert.Equal(t, "jobs.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
var jobs model.SupportPacketJobList
err = yaml.Unmarshal(fileData.Body, &jobs)
require.NoError(t, err)
return &jobs
}
t.Run("no jobs run yet", func(t *testing.T) {
jobs := getJobList(t)
assert.Empty(t, jobs.LDAPSyncJobs)
assert.Empty(t, jobs.DataRetentionJobs)
assert.Empty(t, jobs.MessageExportJobs)
assert.Empty(t, jobs.ElasticPostIndexingJobs)
assert.Empty(t, jobs.ElasticPostAggregationJobs)
assert.Empty(t, jobs.BlevePostIndexingJobs)
assert.Empty(t, jobs.MigrationJobs)
})
t.Run("jobs exist", func(t *testing.T) {
getJob := func(jobType string) *model.Job {
return &model.Job{
Id: model.NewId(),
Type: jobType,
Priority: 1,
CreateAt: model.GetMillis() - 1,
StartAt: model.GetMillis(),
LastActivityAt: model.GetMillis() + 1,
Status: model.JobStatusPending,
Progress: 51,
Data: model.StringMap{"key": "value"},
}
}
// Create some jobs
jobsToCreate := []*model.Job{
getJob(model.JobTypeLdapSync),
getJob(model.JobTypeDataRetention),
getJob(model.JobTypeMessageExport),
getJob(model.JobTypeElasticsearchPostIndexing),
getJob(model.JobTypeElasticsearchPostAggregation),
getJob(model.JobTypeBlevePostIndexing),
getJob(model.JobTypeMigrations),
}
var expectedJobs []*model.Job
for _, job := range jobsToCreate {
// Create the job using the store directly.
// Creating the job at the job service would error as the workers require enterprise code.
rJob, err := th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
expectedJobs = append(expectedJobs, rJob)
}
jobs := getJobList(t)
// Helper to verify job content matches
verifyJob := func(t *testing.T, expected, actual *model.Job) {
t.Helper()
assert.Equal(t, expected.Id, actual.Id)
assert.Equal(t, expected.Type, actual.Type)
assert.Equal(t, expected.Priority, actual.Priority)
assert.Equal(t, expected.CreateAt, actual.CreateAt)
assert.Equal(t, expected.StartAt, actual.StartAt)
assert.Equal(t, expected.LastActivityAt, actual.LastActivityAt)
assert.Equal(t, expected.Status, actual.Status)
assert.Equal(t, expected.Progress, actual.Progress)
assert.Equal(t, expected.Data, actual.Data)
}
// Verify LDAP sync jobs
require.Len(t, jobs.LDAPSyncJobs, 1, "Should have 1 LDAP sync job")
verifyJob(t, expectedJobs[0], jobs.LDAPSyncJobs[0])
// Verify data retention jobs
require.Len(t, jobs.DataRetentionJobs, 1, "Should have 1 data retention job")
verifyJob(t, expectedJobs[1], jobs.DataRetentionJobs[0])
// Verify message export jobs
require.Len(t, jobs.MessageExportJobs, 1, "Should have 1 message export job")
verifyJob(t, expectedJobs[2], jobs.MessageExportJobs[0])
// Verify elasticsearch post indexing jobs
require.Len(t, jobs.ElasticPostIndexingJobs, 1, "Should have 1 elasticsearch post indexing job")
verifyJob(t, expectedJobs[3], jobs.ElasticPostIndexingJobs[0])
// Verify elasticsearch post aggregation jobs
require.Len(t, jobs.ElasticPostAggregationJobs, 1, "Should have 1 elasticsearch post aggregation job")
verifyJob(t, expectedJobs[4], jobs.ElasticPostAggregationJobs[0])
// Verify bleve post indexing jobs
require.Len(t, jobs.BlevePostIndexingJobs, 1, "Should have 1 bleve post indexing job")
verifyJob(t, expectedJobs[5], jobs.BlevePostIndexingJobs[0])
// Verify migration jobs
require.Len(t, jobs.MigrationJobs, 1, "Should have 1 migration job")
verifyJob(t, expectedJobs[6], jobs.MigrationJobs[0])
})
}
func TestGetSupportPacketPermissionsInfo(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.SetPhase2PermissionsMigrationStatus(true)
generatePermissionInfo := func(t *testing.T) *model.SupportPacketPermissionInfo {
t.Helper()
fileData, err := th.App.getSupportPacketPermissionsInfo(th.Context)
require.NotNil(t, fileData)
assert.Equal(t, "permissions.yaml", fileData.Filename)
assert.Positive(t, len(fileData.Body))
assert.NoError(t, err)
var permissions model.SupportPacketPermissionInfo
err = yaml.Unmarshal(fileData.Body, &permissions)
require.NoError(t, err)
return &permissions
}
t.Run("No custom permissions", func(t *testing.T) {
permissions := generatePermissionInfo(t)
assert.Len(t, permissions.Roles, 23)
assert.Empty(t, permissions.Schemes)
})
scheme, appErr := th.App.CreateScheme(&model.Scheme{
Name: "custom_scheme",
DisplayName: "Custom Scheme",
Scope: model.SchemeScopeTeam,
})
require.Nil(t, appErr)
t.Run("with custom scheme", func(t *testing.T) {
permissions := generatePermissionInfo(t)
assert.Len(t, permissions.Roles, 33) // 23 default roles + 10 custom roles from the scheme
require.Len(t, permissions.Schemes, 1)
assert.Equal(t, scheme.Id, permissions.Schemes[0].Id)
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].Name, "Name should be obfuscated")
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].DisplayName, "DisplayName should be obfuscated")
assert.Equal(t, model.FakeSetting, permissions.Schemes[0].Description, "Description should be obfuscated")
})
t.Run("with custom role", func(t *testing.T) {
role, appErr := th.App.CreateRole(&model.Role{
Name: "custom_role",
DisplayName: "Custom Role",
})
require.Nil(t, appErr)
t.Cleanup(func() {
_, appErr := th.App.DeleteRole(role.Id)
require.Nil(t, appErr)
})
permissions := generatePermissionInfo(t)
require.Len(t, permissions.Schemes, 1)
require.Len(t, permissions.Roles, 34) // 23 default roles + 10 custom roles from the scheme + 1 custom role
found := false
for _, r := range permissions.Roles {
// Confirm that sensitive fields are obfuscated
assert.Equal(t, model.FakeSetting, r.DisplayName, "DisplayName should be obfuscated")
assert.Equal(t, model.FakeSetting, r.Description, "Description should be obfuscated")
// Fine the custom role
if r.Id == role.Id {
assert.Equal(t, role.Name, r.Name)
assert.Equal(t, role.CreateAt, r.CreateAt)
assert.Equal(t, role.UpdateAt, r.UpdateAt)
assert.Equal(t, role.DeleteAt, r.DeleteAt)
assert.Equal(t, role.Permissions, r.Permissions)
assert.Equal(t, role.SchemeManaged, r.SchemeManaged)
assert.Equal(t, role.BuiltIn, r.BuiltIn)
found = true
break
}
}
assert.True(t, found)
})
}
func TestGetSupportPacketMetadata(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("Happy path", func(t *testing.T) {
fileData, err := th.App.createSupportPacketMetadata(th.Context)
fileData, err := th.App.getSupportPacketMetadata(th.Context)
require.NoError(t, err)
require.NotNil(t, fileData)
assert.Equal(t, "metadata.yaml", fileData.Filename)

View file

@ -153,7 +153,7 @@ type Client interface {
DownloadExport(ctx context.Context, name string, wr io.Writer, offset int64) (int64, *model.Response, error)
GeneratePresignedURL(ctx context.Context, name string) (*model.PresignURLResponse, *model.Response, error)
ResetSamlAuthDataToEmail(ctx context.Context, includeDeleted bool, dryRun bool, userIDs []string) (int64, *model.Response, error)
GenerateSupportPacket(ctx context.Context) ([]byte, *model.Response, error)
GenerateSupportPacket(ctx context.Context) (io.ReadCloser, string, *model.Response, error)
GetOAuthApps(ctx context.Context, page, perPage int) ([]*model.OAuthApp, *model.Response, error)
GetPreferences(ctx context.Context, userId string) (model.Preferences, *model.Response, error)
GetPreferencesByCategory(ctx context.Context, userId, category string) (model.Preferences, *model.Response, error)

View file

@ -6,8 +6,8 @@ package commands
import (
"context"
"fmt"
"io"
"os"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -81,7 +81,7 @@ func init() {
SystemSetBusyCmd.Flags().UintP("seconds", "s", 3600, "Number of seconds until server is automatically marked as not busy.")
_ = SystemSetBusyCmd.MarkFlagRequired("seconds")
SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Output file name (default \"mattermost_support_packet_YYYY-MM-DD-HH-MM.zip\")")
SystemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
SystemCmd.AddCommand(
SystemGetBusyCmd,
@ -180,23 +180,23 @@ func systemSupportPacketCmdF(c client.Client, cmd *cobra.Command, _ []string) er
return err
}
if filename == "" {
filename = fmt.Sprintf("mattermost_support_packet_%s.zip", time.Now().Format("2006-01-02-03-04"))
}
printer.Print("Downloading Support Packet")
data, _, err := c.GenerateSupportPacket(context.TODO())
data, rFilename, _, err := c.GenerateSupportPacket(context.TODO())
if err != nil {
return fmt.Errorf("unable to fetch Support Packet: %w", err)
}
if filename == "" {
filename = rFilename
}
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create zip file: %w", err)
}
_, err = file.Write(data)
_, err = io.Copy(file, data)
if err != nil {
return fmt.Errorf("failed to write to zip file: %w", err)
}

View file

@ -115,8 +115,6 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
s.Run("Download Support Packet with default filename", func() {
printer.Clean()
s.T().Cleanup(cleanupSupportPacket(s.T()))
err := systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetLines(), 2)
@ -129,12 +127,17 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
entries, err := os.ReadDir(".")
s.Require().NoError(err)
for _, e := range entries {
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
if strings.HasPrefix(e.Name(), "mm_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
b, err := os.ReadFile(e.Name())
s.NoError(err)
s.NotEmpty(b, b)
s.T().Cleanup(func() {
err = os.Remove(e.Name())
s.Require().NoError(err)
})
found = true
}
}
@ -144,14 +147,16 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() {
s.Run("Download Support Packet with custom filename", func() {
printer.Clean()
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
systemSupportPacketCmd := &cobra.Command{}
systemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
err := systemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
s.Require().NoError(err)
s.T().Cleanup(func() {
defer func() {
s.Require().NoError(os.Remove("foo.zip"))
})
}()
err = systemSupportPacketCmdF(s.th.SystemAdminClient, SystemSupportPacketCmd, []string{})
err = systemSupportPacketCmdF(s.th.SystemAdminClient, systemSupportPacketCmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 2)

View file

@ -5,19 +5,17 @@ package commands
import (
"context"
"io"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
)
@ -221,19 +219,6 @@ func (s *MmctlUnitTestSuite) TestServerStatusCmd() {
})
}
func cleanupSupportPacket(t *testing.T) func() {
return func() {
entries, err := os.ReadDir(".")
require.NoError(t, err)
for _, e := range entries {
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
err = os.Remove(e.Name())
assert.NoError(t, err)
}
}
}
}
func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
printer.SetFormat(printer.FormatPlain)
s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) })
@ -241,57 +226,51 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
s.Run("Download Support Packet with default filename", func() {
printer.Clean()
s.T().Cleanup(cleanupSupportPacket(s.T()))
data := []byte("some bytes")
reader := io.NopCloser(strings.NewReader("some bytes"))
s.client.
EXPECT().
GenerateSupportPacket(context.TODO()).
Return(data, &model.Response{}, nil).
Return(reader, "mm_support_packet.zip", &model.Response{}, nil).
Times(1)
defer func() {
err := os.Remove("mm_support_packet.zip")
s.NoError(err)
}()
err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 2)
s.Require().Equal(printer.GetLines()[0], "Downloading Support Packet")
s.Require().Contains(printer.GetLines()[1], "Downloaded Support Packet to ")
s.Require().Equal(printer.GetLines()[1], "Downloaded Support Packet to mm_support_packet.zip")
var found bool
entries, err := os.ReadDir(".")
s.Require().NoError(err)
for _, e := range entries {
if strings.HasPrefix(e.Name(), "mattermost_support_packet_") && strings.HasSuffix(e.Name(), ".zip") {
b, err := os.ReadFile(e.Name())
s.NoError(err)
s.Equal(b, data)
found = true
}
}
s.True(found)
b, err := os.ReadFile("mm_support_packet.zip")
s.NoError(err)
s.Equal(b, []byte("some bytes"))
})
s.Run("Download Support Packet with custom filename", func() {
printer.Clean()
data := []byte("some bytes")
reader := io.NopCloser(strings.NewReader("some bytes"))
s.client.
EXPECT().
GenerateSupportPacket(context.TODO()).
Return(data, &model.Response{}, nil).
Return(reader, "mm_support_packet.zip", &model.Response{}, nil).
Times(1)
err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
systemSupportPacketCmd := &cobra.Command{}
systemSupportPacketCmd.Flags().StringP("output-file", "o", "", "Define the output file name")
err := systemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"})
s.Require().NoError(err)
s.T().Cleanup(func() {
s.Require().NoError(os.Remove("foo.zip"))
})
defer func() {
err = os.Remove("foo.zip")
s.Require().NoError(err)
}()
err = systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})
err = systemSupportPacketCmdF(s.client, systemSupportPacketCmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 2)
@ -300,7 +279,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
b, err := os.ReadFile("foo.zip")
s.Require().NoError(err)
s.Equal(b, data)
s.Equal(string(b), "some bytes")
})
s.Run("Request to the server fails", func() {
@ -309,7 +288,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() {
s.client.
EXPECT().
GenerateSupportPacket(context.TODO()).
Return(nil, &model.Response{}, errors.New("mock error")).
Return(nil, "", &model.Response{}, errors.New("mock error")).
Times(1)
err := systemSupportPacketCmdF(s.client, SystemSupportPacketCmd, []string{})

View file

@ -28,7 +28,7 @@ Options
::
-h, --help help for supportpacket
-o, --output-file string Output file name (default "mattermost_support_packet_YYYY-MM-DD-HH-MM.zip")
-o, --output-file string Define the output file name
Options inherited from parent commands
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -572,13 +572,14 @@ func (mr *MockClientMockRecorder) GeneratePresignedURL(arg0, arg1 interface{}) *
}
// GenerateSupportPacket mocks base method.
func (m *MockClient) GenerateSupportPacket(arg0 context.Context) ([]byte, *model.Response, error) {
func (m *MockClient) GenerateSupportPacket(arg0 context.Context) (io.ReadCloser, string, *model.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GenerateSupportPacket", arg0)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(*model.Response)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(*model.Response)
ret3, _ := ret[3].(error)
return ret0, ret1, ret2, ret3
}
// GenerateSupportPacket indicates an expected call of GenerateSupportPacket.

View file

@ -17,7 +17,6 @@ type LdapInterface interface {
CheckProviderAttributes(c request.CTX, LS *model.LdapSettings, ouser *model.User, patch *model.UserPatch) string
SwitchToLdap(c request.CTX, userID, ldapID, ldapPassword string) *model.AppError
StartSynchronizeJob(c request.CTX, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError)
RunTest(rctx request.CTX) *model.AppError
GetAllLdapUsers(c request.CTX) ([]*model.User, *model.AppError)
MigrateIDAttribute(c request.CTX, toAttribute string) error
GetGroup(rctx request.CTX, groupUID string) (*model.Group, *model.AppError)
@ -26,5 +25,9 @@ type LdapInterface interface {
UpdateProfilePictureIfNecessary(request.CTX, model.User, model.Session)
GetADLdapIdFromSAMLId(c request.CTX, authData string) string
GetSAMLIdFromADLdapId(c request.CTX, authData string) string
}
type LdapDiagnosticInterface interface {
RunTest(rctx request.CTX) *model.AppError
GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error)
}

View file

@ -0,0 +1,85 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
)
// LdapDiagnosticInterface is an autogenerated mock type for the LdapDiagnosticInterface type
type LdapDiagnosticInterface struct {
mock.Mock
}
// GetVendorNameAndVendorVersion provides a mock function with given fields: rctx
func (_m *LdapDiagnosticInterface) GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error) {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for GetVendorNameAndVendorVersion")
}
var r0 string
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(request.CTX) (string, string, error)); ok {
return rf(rctx)
}
if rf, ok := ret.Get(0).(func(request.CTX) string); ok {
r0 = rf(rctx)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(request.CTX) string); ok {
r1 = rf(rctx)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(request.CTX) error); ok {
r2 = rf(rctx)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// RunTest provides a mock function with given fields: rctx
func (_m *LdapDiagnosticInterface) RunTest(rctx request.CTX) *model.AppError {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for RunTest")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
r0 = rf(rctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// NewLdapDiagnosticInterface creates a new instance of LdapDiagnosticInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewLdapDiagnosticInterface(t interface {
mock.TestingT
Cleanup(func())
}) *LdapDiagnosticInterface {
mock := &LdapDiagnosticInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -328,41 +328,6 @@ func (_m *LdapInterface) GetUserAttributes(rctx request.CTX, id string, attribut
return r0, r1
}
// GetVendorNameAndVendorVersion provides a mock function with given fields: rctx
func (_m *LdapInterface) GetVendorNameAndVendorVersion(rctx request.CTX) (string, string, error) {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for GetVendorNameAndVendorVersion")
}
var r0 string
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(request.CTX) (string, string, error)); ok {
return rf(rctx)
}
if rf, ok := ret.Get(0).(func(request.CTX) string); ok {
r0 = rf(rctx)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(request.CTX) string); ok {
r1 = rf(rctx)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(request.CTX) error); ok {
r2 = rf(rctx)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MigrateIDAttribute provides a mock function with given fields: c, toAttribute
func (_m *LdapInterface) MigrateIDAttribute(c request.CTX, toAttribute string) error {
ret := _m.Called(c, toAttribute)
@ -381,26 +346,6 @@ func (_m *LdapInterface) MigrateIDAttribute(c request.CTX, toAttribute string) e
return r0
}
// RunTest provides a mock function with given fields: rctx
func (_m *LdapInterface) RunTest(rctx request.CTX) *model.AppError {
ret := _m.Called(rctx)
if len(ret) == 0 {
panic("no return value specified for RunTest")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
r0 = rf(rctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// StartSynchronizeJob provides a mock function with given fields: c, waitForJobToFinish, includeRemovedMembers
func (_m *LdapInterface) StartSynchronizeJob(c request.CTX, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) {
ret := _m.Called(c, waitForJobToFinish, includeRemovedMembers)

View file

@ -82,7 +82,7 @@ require (
golang.org/x/term v0.22.0
golang.org/x/tools v0.23.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@ -234,7 +234,7 @@ require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect

View file

@ -6586,6 +6586,10 @@
"id": "app.role.check_roles_exist.role_not_found",
"translation": "The provided role does not exist"
},
{
"id": "app.role.delete.app_error",
"translation": "Unable to delete role."
},
{
"id": "app.role.get.app_error",
"translation": "Unable to get role."

View file

@ -32,7 +32,7 @@ require (
golang.org/x/oauth2 v0.21.0
golang.org/x/text v0.16.0
golang.org/x/tools v0.23.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@ -70,7 +70,7 @@ require (
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
// Hack to prevent the willf/bitset module from being upgraded to 1.2.0.

View file

@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net"
"net/http"
@ -4778,18 +4779,19 @@ func (c *Client4) GetFileInfosForPostIncludeDeleted(ctx context.Context, postId
// General/System Section
// GenerateSupportPacket generates and downloads a Support Packet.
func (c *Client4) GenerateSupportPacket(ctx context.Context) ([]byte, *Response, error) {
// It returns a ReadCloser to the packet and the filename. The caller needs to close the ReadCloser.
func (c *Client4) GenerateSupportPacket(ctx context.Context) (io.ReadCloser, string, *Response, error) {
r, err := c.DoAPIGet(ctx, c.systemRoute()+"/support_packet", "")
if err != nil {
return nil, BuildResponse(r), err
return nil, "", BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Disposition"))
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, "", r.StatusCode).Wrap(err)
return nil, "", BuildResponse(r), fmt.Errorf("could not parse Content-Disposition header: %w", err)
}
return data, BuildResponse(r), nil
return r.Body, params["filename"], BuildResponse(r), nil
}
// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above.

View file

@ -5,6 +5,8 @@ package model
import (
"net/http"
"github.com/mattermost/mattermost/server/public/utils/timeutils"
)
const (
@ -103,6 +105,75 @@ func (j *Job) Auditable() map[string]interface{} {
}
}
func (j *Job) MarshalYAML() (any, error) {
return struct {
Id string `yaml:"id"`
Type string `yaml:"type"`
Priority int64 `yaml:"priority"`
CreateAt string `yaml:"create_at"`
StartAt string `yaml:"start_at"`
LastActivityAt string `yaml:"last_activity_at"`
Status string `yaml:"status"`
Progress int64 `yaml:"progress"`
Data StringMap `yaml:"data"`
}{
Id: j.Id,
Type: j.Type,
Priority: j.Priority,
CreateAt: timeutils.FormatMillis(j.CreateAt),
StartAt: timeutils.FormatMillis(j.StartAt),
LastActivityAt: timeutils.FormatMillis(j.LastActivityAt),
Status: j.Status,
Progress: j.Progress,
Data: j.Data,
}, nil
}
func (j *Job) UnmarshalYAML(unmarshal func(interface{}) error) error {
out := struct {
Id string `yaml:"id"`
Type string `yaml:"type"`
Priority int64 `yaml:"priority"`
CreateAt string `yaml:"create_at"`
StartAt string `yaml:"start_at"`
LastActivityAt string `yaml:"last_activity_at"`
Status string `yaml:"status"`
Progress int64 `yaml:"progress"`
Data StringMap `yaml:"data"`
}{}
err := unmarshal(&out)
if err != nil {
return err
}
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
if err != nil {
return err
}
updateAt, err := timeutils.ParseFormatedMillis(out.StartAt)
if err != nil {
return err
}
deleteAt, err := timeutils.ParseFormatedMillis(out.LastActivityAt)
if err != nil {
return err
}
*j = Job{
Id: out.Id,
Type: out.Type,
Priority: out.Priority,
CreateAt: createAt,
StartAt: updateAt,
LastActivityAt: deleteAt,
Status: out.Status,
Progress: out.Progress,
Data: out.Data,
}
return nil
}
func (j *Job) IsValid() *AppError {
if !IsValidId(j.Id) {
return NewAppError("Job.IsValid", "model.job.is_valid.id.app_error", nil, "id="+j.Id, http.StatusBadRequest)

View file

@ -13,7 +13,7 @@ import (
"github.com/blang/semver/v4"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
type PluginOption struct {

View file

@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func TestIsValid(t *testing.T) {

View file

@ -7,7 +7,7 @@ import (
"fmt"
"github.com/blang/semver/v4"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
type PacketType string

View file

@ -7,7 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func TestPacketMetadataValidate(t *testing.T) {

View file

@ -6,6 +6,8 @@ package model
import (
"fmt"
"strings"
"github.com/mattermost/mattermost/server/public/utils/timeutils"
)
// SysconsoleAncillaryPermissions maps the non-sysconsole permissions required by each sysconsole view.
@ -438,6 +440,84 @@ func (r *Role) Auditable() map[string]interface{} {
}
}
func (r *Role) Sanitize() {
r.DisplayName = FakeSetting
r.Description = FakeSetting
}
func (r *Role) MarshalYAML() (any, error) {
return struct {
Id string `yaml:"id"`
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
CreateAt string `yaml:"create_at"`
UpdateAt string `yaml:"update_at"`
DeleteAt string `yaml:"delete_at"`
Permissions []string `yaml:"permissions"`
SchemeManaged bool `yaml:"scheme_managed"`
BuiltIn bool `yaml:"built_in"`
}{
Id: r.Id,
Name: r.Name,
DisplayName: r.DisplayName,
Description: r.Description,
CreateAt: timeutils.FormatMillis(r.CreateAt),
UpdateAt: timeutils.FormatMillis(r.UpdateAt),
DeleteAt: timeutils.FormatMillis(r.DeleteAt),
Permissions: r.Permissions,
SchemeManaged: r.SchemeManaged,
BuiltIn: r.BuiltIn,
}, nil
}
func (r *Role) UnmarshalYAML(unmarshal func(interface{}) error) error {
out := struct {
Id string `yaml:"id"`
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
CreateAt string `yaml:"create_at"`
UpdateAt string `yaml:"update_at"`
DeleteAt string `yaml:"delete_at"`
Permissions []string `yaml:"permissions"`
SchemeManaged bool `yaml:"scheme_managed"`
BuiltIn bool `yaml:"built_in"`
}{}
err := unmarshal(&out)
if err != nil {
return err
}
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
if err != nil {
return err
}
updateAt, err := timeutils.ParseFormatedMillis(out.UpdateAt)
if err != nil {
return err
}
deleteAt, err := timeutils.ParseFormatedMillis(out.DeleteAt)
if err != nil {
return err
}
*r = Role{
Id: out.Id,
Name: out.Name,
DisplayName: out.DisplayName,
Description: out.Description,
CreateAt: createAt,
UpdateAt: updateAt,
DeleteAt: deleteAt,
Permissions: out.Permissions,
SchemeManaged: out.SchemeManaged,
BuiltIn: out.BuiltIn,
}
return nil
}
type RolePatch struct {
Permissions *[]string `json:"permissions"`
}

View file

@ -6,6 +6,8 @@ package model
import (
"fmt"
"regexp"
"github.com/mattermost/mattermost/server/public/utils/timeutils"
)
const (
@ -62,6 +64,117 @@ func (scheme *Scheme) Auditable() map[string]interface{} {
}
}
func (scheme *Scheme) Sanitize() {
scheme.Name = FakeSetting
scheme.DisplayName = FakeSetting
scheme.Description = FakeSetting
}
func (scheme *Scheme) MarshalYAML() (any, error) {
return struct {
Id string `yaml:"id"`
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
CreateAt string `yaml:"create_at"`
UpdateAt string `yaml:"update_at"`
DeleteAt string `yaml:"delete_at"`
Scope string `yaml:"scope"`
DefaultTeamAdminRole string `yaml:"default_team_admin_role"`
DefaultTeamUserRole string `yaml:"default_team_user_role"`
DefaultChannelAdminRole string `yaml:"default_channel_admin_role"`
DefaultChannelUserRole string `yaml:"default_channel_user_role"`
DefaultTeamGuestRole string `yaml:"default_team_guest_role"`
DefaultChannelGuestRole string `yaml:"default_channel_guest_role"`
DefaultPlaybookAdminRole string `yaml:"default_playbook_admin_role"`
DefaultPlaybookMemberRole string `yaml:"default_playbook_member_role"`
DefaultRunAdminRole string `yaml:"default_run_admin_role"`
DefaultRunMemberRole string `yaml:"default_run_member_role"`
}{
Id: scheme.Id,
Name: scheme.Name,
DisplayName: scheme.DisplayName,
Description: scheme.Description,
CreateAt: timeutils.FormatMillis(scheme.CreateAt),
UpdateAt: timeutils.FormatMillis(scheme.UpdateAt),
DeleteAt: timeutils.FormatMillis(scheme.DeleteAt),
Scope: scheme.Scope,
DefaultTeamAdminRole: scheme.DefaultTeamAdminRole,
DefaultTeamUserRole: scheme.DefaultTeamUserRole,
DefaultChannelAdminRole: scheme.DefaultChannelAdminRole,
DefaultChannelUserRole: scheme.DefaultChannelUserRole,
DefaultTeamGuestRole: scheme.DefaultTeamGuestRole,
DefaultChannelGuestRole: scheme.DefaultChannelGuestRole,
DefaultPlaybookAdminRole: scheme.DefaultPlaybookAdminRole,
DefaultPlaybookMemberRole: scheme.DefaultPlaybookMemberRole,
DefaultRunAdminRole: scheme.DefaultRunAdminRole,
DefaultRunMemberRole: scheme.DefaultRunMemberRole,
}, nil
}
func (scheme *Scheme) UnmarshalYAML(unmarshal func(interface{}) error) error {
out := struct {
Id string `yaml:"id"`
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
CreateAt string `yaml:"create_at"`
UpdateAt string `yaml:"update_at"`
DeleteAt string `yaml:"delete_at"`
Scope string `yaml:"scope"`
DefaultTeamAdminRole string `yaml:"default_team_admin_role"`
DefaultTeamUserRole string `yaml:"default_team_user_role"`
DefaultChannelAdminRole string `yaml:"default_channel_admin_role"`
DefaultChannelUserRole string `yaml:"default_channel_user_role"`
DefaultTeamGuestRole string `yaml:"default_team_guest_role"`
DefaultChannelGuestRole string `yaml:"default_channel_guest_role"`
DefaultPlaybookAdminRole string `yaml:"default_playbook_admin_role"`
DefaultPlaybookMemberRole string `yaml:"default_playbook_member_role"`
DefaultRunAdminRole string `yaml:"default_run_admin_role"`
DefaultRunMemberRole string `yaml:"default_run_member_role"`
}{}
err := unmarshal(&out)
if err != nil {
return err
}
createAt, err := timeutils.ParseFormatedMillis(out.CreateAt)
if err != nil {
return err
}
updateAt, err := timeutils.ParseFormatedMillis(out.UpdateAt)
if err != nil {
return err
}
deleteAt, err := timeutils.ParseFormatedMillis(out.DeleteAt)
if err != nil {
return err
}
*scheme = Scheme{
Id: out.Id,
Name: out.Name,
DisplayName: out.DisplayName,
Description: out.Description,
CreateAt: createAt,
UpdateAt: updateAt,
DeleteAt: deleteAt,
Scope: out.Scope,
DefaultTeamAdminRole: out.DefaultTeamAdminRole,
DefaultTeamUserRole: out.DefaultTeamUserRole,
DefaultChannelAdminRole: out.DefaultChannelAdminRole,
DefaultChannelUserRole: out.DefaultChannelUserRole,
DefaultTeamGuestRole: out.DefaultTeamGuestRole,
DefaultChannelGuestRole: out.DefaultChannelGuestRole,
DefaultPlaybookAdminRole: out.DefaultPlaybookAdminRole,
DefaultPlaybookMemberRole: out.DefaultPlaybookMemberRole,
DefaultRunAdminRole: out.DefaultRunAdminRole,
DefaultRunMemberRole: out.DefaultRunMemberRole,
}
return nil
}
type SchemePatch struct {
Name *string `json:"name"`
DisplayName *string `json:"display_name"`

View file

@ -9,72 +9,120 @@ import (
)
const (
SupportPacketErrorFile = "warning.txt"
CurrentSupportPacketVersion = 1
SupportPacketErrorFile = "warning.txt"
)
type SupportPacket struct {
/* Build information */
type SupportPacketDiagnostics struct {
Version int `yaml:"version"`
ServerOS string `yaml:"server_os"`
ServerArchitecture string `yaml:"server_architecture"`
ServerVersion string `yaml:"server_version"`
BuildHash string `yaml:"build_hash"`
License struct {
Company string `yaml:"company"`
Users int `yaml:"users"`
SkuShortName string `yaml:"sku_short_name"`
IsTrial bool `yaml:"is_trial,omitempty"`
IsGovSKU bool `yaml:"is_gov_sku,omitempty"`
} `yaml:"license"`
/* DB */
Server struct {
OS string `yaml:"os"`
Architecture string `yaml:"architecture"`
Hostname string `yaml:"hostname"`
Version string `yaml:"version"`
BuildHash string `yaml:"build_hash"`
InstallationType string `yaml:"installation_type"`
} `yaml:"server"`
DatabaseType string `yaml:"database_type"`
DatabaseVersion string `yaml:"database_version"`
DatabaseSchemaVersion string `yaml:"database_schema_version"`
WebsocketConnections int `yaml:"websocket_connections"`
MasterDbConnections int `yaml:"master_db_connections"`
ReplicaDbConnections int `yaml:"read_db_connections"`
Config struct {
Source string `yaml:"store_type"`
} `yaml:"config"`
/* Cluster */
Database struct {
Type string `yaml:"type"`
Version string `yaml:"version"`
SchemaVersion string `yaml:"schema_version"`
MasterConnectios int `yaml:"master_connections"`
ReplicaConnectios int `yaml:"replica_connections"`
SearchConnections int `yaml:"search_connections"`
} `yaml:"database"`
ClusterID string `yaml:"cluster_id"`
FileStore struct {
Status string `yaml:"file_status"`
Error string `yaml:"erorr,omitempty"`
Driver string `yaml:"file_driver"`
} `yaml:"file_store"`
/* File store */
Websocket struct {
Connections int `yaml:"connections"`
} `yaml:"websocket"`
FileDriver string `yaml:"file_driver"`
FileStatus string `yaml:"file_status"`
Cluster struct {
ID string `yaml:"id"`
NumberOfNodes int `yaml:"number_of_nodes"`
} `yaml:"cluster"`
/* LDAP */
LDAP struct {
Status string `yaml:"status,omitempty"`
Error string `yaml:"erorr,omitempty"`
ServerName string `yaml:"server_name,omitempty"`
ServerVersion string `yaml:"server_version,omitempty"`
} `yaml:"ldap"`
LdapVendorName string `yaml:"ldap_vendor_name,omitempty"`
LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"`
ElasticSearch struct {
ServerVersion string `yaml:"server_version,omitempty"`
ServerPlugins []string `yaml:"server_plugins,omitempty"`
} `yaml:"elastic"`
}
/* Elastic Search */
ElasticServerVersion string `yaml:"elastic_server_version,omitempty"`
ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"`
/* License */
LicenseTo string `yaml:"license_to"`
LicenseSupportedUsers int `yaml:"license_supported_users"`
LicenseIsTrial bool `yaml:"license_is_trial,omitempty"`
/* Server stats */
ActiveUsers int `yaml:"active_users"`
DailyActiveUsers int `yaml:"daily_active_users"`
MonthlyActiveUsers int `yaml:"monthly_active_users"`
InactiveUserCount int `yaml:"inactive_user_count"`
TotalPosts int `yaml:"total_posts"`
TotalChannels int `yaml:"total_channels"`
TotalTeams int `yaml:"total_teams"`
/* Jobs */
type SupportPacketStats struct {
RegisteredUsers int64 `yaml:"registered_users"`
ActiveUsers int64 `yaml:"active_users"`
DailyActiveUsers int64 `yaml:"daily_active_users"`
MonthlyActiveUsers int64 `yaml:"monthly_active_users"`
DeactivatedUsers int64 `yaml:"deactivated_users"`
Guests int64 `yaml:"guests"`
BotAccounts int64 `yaml:"bot_accounts"`
Posts int64 `yaml:"posts"`
Channels int64 `yaml:"channels"`
Teams int64 `yaml:"teams"`
SlashCommands int64 `yaml:"slash_commands"`
IncomingWebhooks int64 `yaml:"incoming_webhooks"`
OutgoingWebhooks int64 `yaml:"outgoing_webhooks"`
}
// SupportPacketJobList contains the list of latest run enterprise job runs.
// It is included in the Support Packet.
type SupportPacketJobList struct {
LDAPSyncJobs []*Job `yaml:"ldap_sync_jobs"`
DataRetentionJobs []*Job `yaml:"data_retention_jobs"`
MessageExportJobs []*Job `yaml:"message_export_jobs"`
ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"`
ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"`
BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"`
LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"`
MigrationJobs []*Job `yaml:"migration_jobs"`
}
// SupportPacketPermissionInfo contains the list of schemes and the list of roles.
// It is included in the Support Packet.
type SupportPacketPermissionInfo struct {
Roles []*Role `yaml:"roles"`
Schemes []*Scheme `yaml:"schemes"`
}
// SupportPacketConfig contains the Mattermost configuration. In contrast to [Config], it also contains the list of Feature Flags.
// It is included in the Support Packet.
type SupportPacketConfig struct {
*Config
FeatureFlags FeatureFlags `json:"FeatureFlags"`
}
// SupportPacketPluginList contains the list of enabled and disabled plugins.
// It is included in the Support Packet.
type SupportPacketPluginList struct {
Enabled []Manifest `json:"enabled"`
Disabled []Manifest `json:"disabled"`
}
type FileData struct {
Filename string
Body []byte

View file

@ -9,7 +9,7 @@ import (
"github.com/blang/semver/v4"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"

View file

@ -8,7 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"

View file

@ -6,6 +6,8 @@ package utils
import (
"os"
"path/filepath"
"regexp"
"strings"
)
func CommonBaseSearchPaths() []string {
@ -111,3 +113,25 @@ func FindDirRelBinary(dir string) (string, bool) {
}
return found, true
}
// Valid characters are: alphanumeric, dash, underscore
var safeFileNameRegex = regexp.MustCompile(`[^\w\-\_]`)
// SanitizeFileName takes a string and returns a safe file name without an extension.
func SanitizeFileName(input string) string {
// Trim leading or trailing dots or spaces
safeName := strings.Trim(input, ". ")
// Replace dots with nothing
safeName = strings.ReplaceAll(safeName, ".", "")
// Replace all invalid characters with an underscore
safeName = safeFileNameRegex.ReplaceAllString(safeName, "_")
// Limit length
const maxLength = 100
if len(safeName) > maxLength {
safeName = safeName[:maxLength]
}
return safeName
}

View file

@ -7,12 +7,69 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSanitizeFileName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "normal characters",
input: "normal-file_name",
expected: "normal-file_name",
},
{
name: "special characters",
input: "file*name@#$",
expected: "file_name___",
},
{
name: "spaces",
input: "my file name",
expected: "my_file_name",
},
{
name: "leading/trailing dots and spaces",
input: " .filename. ",
expected: "filename",
},
{
name: "very long filename",
input: strings.Repeat("a", 150) + ".txt",
expected: strings.Repeat("a", 100),
},
{
name: "unicode characters",
input: "résumé",
expected: "r_sum_",
},
{
name: "german umlaute",
input: "äëïöüß",
expected: "______",
},
{
name: "empty string",
input: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SanitizeFileName(tt.input)
assert.Equal(t, tt.expected, result, tt.name)
})
}
}
func TestFindFile(t *testing.T) {
t.Run("files from various paths", func(t *testing.T) {
// Create the following directory structure:

View file

@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package timeutils
import (
"time"
)
const (
RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
)
func FormatMillis(millis int64) string {
return time.UnixMilli(millis).Format(RFC3339Milli)
}
func ParseFormatedMillis(s string) (millis int64, err error) {
if s == "" {
return 0, nil
}
t, err := time.Parse(RFC3339Milli, s)
if err != nil {
return 0, err
}
return t.UnixMilli(), nil
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package timeutils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormatMillis(t *testing.T) {
t.Run("zero time", func(t *testing.T) {
result := FormatMillis(0)
// The concrete time depends on the timezone, so we can't test the exact time
assert.Contains(t, result, "1970-01-01")
assert.Contains(t, result, "00:00")
})
t.Run("positive time", func(t *testing.T) {
result := FormatMillis(1609459200000) // 2021-01-01 00:00:00 UTC
assert.Contains(t, result, "2021-01-01")
assert.Contains(t, result, "00:00")
})
t.Run("negative time", func(t *testing.T) {
result := FormatMillis(-1609459200000) // 1919-01-01 00:00:00 UTC
assert.Contains(t, result, "1919-01-01")
assert.Contains(t, result, "00:00")
})
}
func TestParseFormatedMillis(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
result, err := ParseFormatedMillis("")
assert.NoError(t, err)
assert.Equal(t, int64(0), result)
})
t.Run("valid timestamp", func(t *testing.T) {
result, err := ParseFormatedMillis("2021-01-01T00:00:00.000Z")
assert.NoError(t, err)
assert.Equal(t, int64(1609459200000), result)
})
t.Run("invalid format", func(t *testing.T) {
result, err := ParseFormatedMillis("2021-01-01")
assert.Error(t, err)
assert.Equal(t, int64(0), result)
})
t.Run("invalid date", func(t *testing.T) {
result, err := ParseFormatedMillis("2021-13-01T00:00:00.000Z")
assert.Error(t, err)
assert.Equal(t, int64(0), result)
})
}

View file

@ -92,8 +92,8 @@ export default class CommercialSupportModal extends React.PureComponent<Props, S
extractFilename = (input: string | null): string => {
// construct the expected filename in case of an error in the header
const formattedDate = (moment(new Date())).format('YYYY-MM-DD-HH-mm');
const presumedFileName = `mattermost_support_packet_${formattedDate}.zip`;
const formattedDate = (moment(new Date())).format('YYYY-MM-DDTHH-mm');
const presumedFileName = `mm_support_packet_${formattedDate}.zip`;
if (input === null) {
return presumedFileName;