[MM-55296] Added warning in Workspace dashboard if Mattermost is running as root (#27999)
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"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) {
|
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
reqs := c.App.Config().ClientRequirements
|
reqs := c.App.Config().ClientRequirements
|
||||||
|
|
||||||
s := make(map[string]string)
|
s := make(map[string]any)
|
||||||
s[model.STATUS] = model.StatusOk
|
s[model.STATUS] = model.StatusOk
|
||||||
s["AndroidLatestVersion"] = reqs.AndroidLatestVersion
|
s["AndroidLatestVersion"] = reqs.AndroidLatestVersion
|
||||||
s["AndroidMinVersion"] = reqs.AndroidMinVersion
|
s["AndroidMinVersion"] = reqs.AndroidMinVersion
|
||||||
@@ -196,9 +197,20 @@ func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
s[model.STATUS] = model.StatusUnhealthy
|
s[model.STATUS] = model.StatusUnhealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(model.STATUS, s[model.STATUS])
|
if res, ok := s[model.STATUS].(string); ok {
|
||||||
w.Header().Set(dbStatusKey, s[dbStatusKey])
|
w.Header().Set(model.STATUS, res)
|
||||||
w.Header().Set(filestoreStatusKey, s[filestoreStatusKey])
|
}
|
||||||
|
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 != "" {
|
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" {
|
if s[model.STATUS] != model.StatusOk && r.FormValue("use_rest_semantics") != "true" {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
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) {
|
func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -61,34 +61,46 @@ func TestGetPing(t *testing.T) {
|
|||||||
|
|
||||||
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
|
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
|
||||||
th.App.ReloadConfig()
|
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.NoError(t, err)
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
respBytes, err := io.ReadAll(resp.Body)
|
_, ok := respMap["TestFeatureFlag"]
|
||||||
require.NoError(t, err)
|
assert.Equal(t, false, ok)
|
||||||
respString := string(respBytes)
|
|
||||||
require.NotContains(t, respString, "TestFeatureFlag")
|
|
||||||
|
|
||||||
// Run the environment variable override code to test
|
// Run the environment variable override code to test
|
||||||
os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "testvalueunique")
|
os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "testvalueunique")
|
||||||
defer os.Unsetenv("MM_FEATUREFLAGS_TESTFEATURE")
|
defer os.Unsetenv("MM_FEATUREFLAGS_TESTFEATURE")
|
||||||
th.App.ReloadConfig()
|
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.NoError(t, err)
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
respBytes, err = io.ReadAll(resp.Body)
|
_, ok = respMap["TestFeatureFlag"]
|
||||||
require.NoError(t, err)
|
assert.Equal(t, true, ok)
|
||||||
respString = string(respBytes)
|
|
||||||
require.Contains(t, respString, "testvalue")
|
|
||||||
}, "ping feature flag test")
|
}, "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.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
|
||||||
th.App.ReloadConfig()
|
th.App.ReloadConfig()
|
||||||
resp, err := client.DoAPIGet(context.Background(), "/system/ping?device_id=platform:id", "")
|
resp, err := client.DoAPIGet(context.Background(), "/system/ping?device_id=platform:id", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
var respMap map[string]string
|
var respMap map[string]any
|
||||||
err = json.NewDecoder(resp.Body).Decode(&respMap)
|
err = json.NewDecoder(resp.Body).Decode(&respMap)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "unknown", respMap["CanReceiveNotifications"]) // Unrecognized platform
|
assert.Equal(t, "unknown", respMap["CanReceiveNotifications"]) // Unrecognized platform
|
||||||
|
|||||||
@@ -76,6 +76,39 @@ const sessionLength = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description This checks to see if Mattermost is running as root.
|
||||||
|
*/
|
||||||
|
const rootUserCheck = async (
|
||||||
|
config: Partial<AdminConfig>,
|
||||||
|
formatMessage: ReturnType<typeof useIntl>['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 (
|
const fileStorage = async (
|
||||||
config: Partial<AdminConfig>,
|
config: Partial<AdminConfig>,
|
||||||
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
|
||||||
@@ -119,6 +152,7 @@ export const runConfigChecks = async (
|
|||||||
ssl,
|
ssl,
|
||||||
sessionLength,
|
sessionLength,
|
||||||
fileStorage,
|
fileStorage,
|
||||||
|
rootUserCheck,
|
||||||
];
|
];
|
||||||
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -2043,6 +2043,8 @@
|
|||||||
"admin.reporting.workspace_optimization.configuration.file_storage.cta": "Config file storage",
|
"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.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.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.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.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",
|
"admin.reporting.workspace_optimization.configuration.session_length.title": "Session lengths is set to default",
|
||||||
|
|||||||
@@ -2514,6 +2514,7 @@ export default class Client4 {
|
|||||||
ActiveSearchBackend: string;
|
ActiveSearchBackend: string;
|
||||||
database_status: string;
|
database_status: string;
|
||||||
filestore_status: string;
|
filestore_status: string;
|
||||||
|
root_status: boolean;
|
||||||
}>(
|
}>(
|
||||||
`${this.getBaseRoute()}/system/ping${buildQueryString({get_server_status: getServerStatus, device_id: deviceId, use_rest_semantics: true})}`,
|
`${this.getBaseRoute()}/system/ping${buildQueryString({get_server_status: getServerStatus, device_id: deviceId, use_rest_semantics: true})}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user