diff --git a/server/channels/api4/system.go b/server/channels/api4/system.go index bf1123f36b..de08e1a39a 100644 --- a/server/channels/api4/system.go +++ b/server/channels/api4/system.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "os" "path" "reflect" "runtime" @@ -144,7 +145,7 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) { reqs := c.App.Config().ClientRequirements - s := make(map[string]string) + s := make(map[string]any) s[model.STATUS] = model.StatusOk s["AndroidLatestVersion"] = reqs.AndroidLatestVersion s["AndroidMinVersion"] = reqs.AndroidMinVersion @@ -196,9 +197,20 @@ func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) { s[model.STATUS] = model.StatusUnhealthy } - w.Header().Set(model.STATUS, s[model.STATUS]) - w.Header().Set(dbStatusKey, s[dbStatusKey]) - w.Header().Set(filestoreStatusKey, s[filestoreStatusKey]) + if res, ok := s[model.STATUS].(string); ok { + w.Header().Set(model.STATUS, res) + } + if res, ok := s[dbStatusKey].(string); ok { + w.Header().Set(dbStatusKey, res) + } + if res, ok := s[filestoreStatusKey].(string); ok { + w.Header().Set(filestoreStatusKey, res) + } + + // Checking if mattermost is running as root, if the user is system admin + if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { + s["root_status"] = os.Geteuid() == 0 + } } if deviceID := r.FormValue("device_id"); deviceID != "" { @@ -210,7 +222,8 @@ func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) { if s[model.STATUS] != model.StatusOk && r.FormValue("use_rest_semantics") != "true" { w.WriteHeader(http.StatusInternalServerError) } - w.Write([]byte(model.MapToJSON(s))) + + w.Write(model.ToJSON(s)) } func testEmail(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index d89b1c3c04..5096b39989 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -61,34 +61,46 @@ func TestGetPing(t *testing.T) { th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { th.App.ReloadConfig() - resp, err := client.DoAPIGet(context.Background(), "/system/ping", "") + respMap, resp, err := client.GetPingWithOptions(context.Background(), model.SystemPingOptions{}) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) - respBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - respString := string(respBytes) - require.NotContains(t, respString, "TestFeatureFlag") + _, ok := respMap["TestFeatureFlag"] + assert.Equal(t, false, ok) // Run the environment variable override code to test os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "testvalueunique") defer os.Unsetenv("MM_FEATUREFLAGS_TESTFEATURE") th.App.ReloadConfig() - resp, err = client.DoAPIGet(context.Background(), "/system/ping", "") + respMap, resp, err = client.GetPingWithOptions(context.Background(), model.SystemPingOptions{}) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) - respBytes, err = io.ReadAll(resp.Body) - require.NoError(t, err) - respString = string(respBytes) - require.Contains(t, respString, "testvalue") + _, ok = respMap["TestFeatureFlag"] + assert.Equal(t, true, ok) }, "ping feature flag test") + t.Run("ping root_status test", func(t *testing.T) { + respMap, resp, err := th.SystemAdminClient.GetPingWithOptions(context.Background(), model.SystemPingOptions{FullStatus: true}) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + _, ok := respMap["root_status"] + assert.Equal(t, true, ok) + }) + + t.Run("ping root_status test with client user", func(t *testing.T) { + respMap, resp, err := th.Client.GetPingWithOptions(context.Background(), model.SystemPingOptions{FullStatus: true}) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + _, ok := respMap["root_status"] + assert.Equal(t, false, ok) + }) + th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { th.App.ReloadConfig() resp, err := client.DoAPIGet(context.Background(), "/system/ping?device_id=platform:id", "") require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) - var respMap map[string]string + var respMap map[string]any err = json.NewDecoder(resp.Body).Decode(&respMap) require.NoError(t, err) assert.Equal(t, "unknown", respMap["CanReceiveNotifications"]) // Unrecognized platform diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts index 72c1e5f9ec..5a5f1c29a0 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts @@ -76,6 +76,39 @@ const sessionLength = ( }; }; +/** + * + * @description This checks to see if Mattermost is running as root. + */ +const rootUserCheck = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const fetchRootStatus = async () => { + const result = await Client4.ping(true); + return result.root_status ? ItemStatus.WARNING : ItemStatus.OK; + }; + + const status = await fetchRootStatus(); + + return { + id: 'root_status,', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.root_status.title', + defaultMessage: 'Mattermost is running as root', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.root_status.description', + defaultMessage: 'Running Mattermost as root is not recommended. Please use a non-root user.', + }), + telemetryAction: 'root_status', + status, + scoreImpact: 25, + impactModifier: impactModifiers[status], + }; +}; + const fileStorage = async ( config: Partial, formatMessage: ReturnType['formatMessage'], @@ -119,6 +152,7 @@ export const runConfigChecks = async ( ssl, sessionLength, fileStorage, + rootUserCheck, ]; const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); return results; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index a5540b9e71..6d4437e8fd 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -2043,6 +2043,8 @@ "admin.reporting.workspace_optimization.configuration.file_storage.cta": "Config file storage", "admin.reporting.workspace_optimization.configuration.file_storage.description": "Check your file storage settings to ensure your Mattermost workspace has access to the configured file storage.", "admin.reporting.workspace_optimization.configuration.file_storage.title": "File storage access is faulty.", + "admin.reporting.workspace_optimization.configuration.root_status.description": "Running Mattermost as root is not recommended. Please use a non-root user.", + "admin.reporting.workspace_optimization.configuration.root_status.title": "Mattermost is running as root", "admin.reporting.workspace_optimization.configuration.session_length.cta": "Configure session length", "admin.reporting.workspace_optimization.configuration.session_length.description": "Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization's security policies.", "admin.reporting.workspace_optimization.configuration.session_length.title": "Session lengths is set to default", diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 4025622d45..20df1d893a 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -2514,6 +2514,7 @@ export default class Client4 { ActiveSearchBackend: string; database_status: string; filestore_status: string; + root_status: boolean; }>( `${this.getBaseRoute()}/system/ping${buildQueryString({get_server_status: getServerStatus, device_id: deviceId, use_rest_semantics: true})}`, {method: 'get'},