From 0d89ff5d0e23cd335d5a849bbc2748d3b310ab53 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Tue, 19 May 2020 18:20:41 +0200 Subject: [PATCH] Mm 23710 mmctl local mode (#14561) * [MM-24146] Add unix socket listener for mmctl local mode (#14296) * add unix socket listener for mmctl local mode * add a constant for local-mode socket path * reflect review comments * [MM-24401] Base approach for Local Mode (#14333) * add unix socket listener for mmctl local mode * First working PoC * Adds the channel list endpoint * Add team list endpoint * Add a LocalClient to the api test helper and start local mode * Add helper to test with both SystemAdmin and Local clients * Add some docs * Adds TestForAllClients test helper * Incorporating @ashishbhate's proposal for adding test names to the helpers * Fix init errors after merge * Adds create channel tests * Always init local mode to allow for enabling-disabling it via config * Check the RemoteAddr of the request before marking session as local * Mark the request as errored if it's local and the origin is remote * Set the socket permissions to read/write when initialising * Fix linter * Replace RemoteAddr check to ditch connections with the IP:PORT shape Co-authored-by: Ibrahim Serdar Acikgoz * Fix translations order * [MM-24832] Migrate plugin endpoints to local mode (#14543) * [MM-24832] Migrate plugin endpoints to local mode * Fix client reference in helper * [MM-24776] Migrate config endpoints to local mode (#14544) * [MM-24776] Migrate get config endpoint to local mode * [MM-24777] Migrate update config endpoint to local mode * Fix update config to bypass RestrictSystemAdmin flag * Add patchConfig endpoint * MM-24774/MM-24755: local mode for addLicense and removeLicense (#14491) Automatic Merge Co-authored-by: Ibrahim Serdar Acikgoz Co-authored-by: Ashish Bhate --- api4/api.go | 30 ++ api4/apitestlib.go | 68 +++- api4/channel_local.go | 41 +++ api4/channel_test.go | 50 +-- api4/config_local.go | 112 ++++++ api4/config_test.go | 334 +++++++++-------- api4/handlers.go | 28 ++ api4/license_local.go | 95 +++++ api4/license_test.go | 29 +- api4/plugin_local.go | 13 + api4/plugin_test.go | 576 +++++++++++++++--------------- api4/team_local.go | 8 + app/authorization.go | 3 + app/server.go | 45 +++ cmd/mattermost/commands/server.go | 1 + i18n/en.json | 4 + model/config.go | 12 + model/session.go | 7 + web/handlers.go | 16 + 19 files changed, 1008 insertions(+), 464 deletions(-) create mode 100644 api4/channel_local.go create mode 100644 api4/config_local.go create mode 100644 api4/license_local.go create mode 100644 api4/plugin_local.go create mode 100644 api4/team_local.go diff --git a/api4/api.go b/api4/api.go index 26acf42383..b9ccfb8c7f 100644 --- a/api4/api.go +++ b/api4/api.go @@ -255,6 +255,36 @@ func Init(configservice configservice.ConfigService, globalOptionsFunc app.AppOp return api } +func InitLocal(configservice configservice.ConfigService, globalOptionsFunc app.AppOptionCreator, root *mux.Router) *API { + api := &API{ + ConfigService: configservice, + GetGlobalAppOptions: globalOptionsFunc, + BaseRoutes: &Routes{}, + } + + api.BaseRoutes.Root = root + api.BaseRoutes.ApiRoot = root.PathPrefix(model.API_URL_SUFFIX).Subrouter() + + api.BaseRoutes.Teams = api.BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter() + + api.BaseRoutes.Channels = api.BaseRoutes.ApiRoot.PathPrefix("/channels").Subrouter() + + api.BaseRoutes.License = api.BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter() + + api.BaseRoutes.Plugins = api.BaseRoutes.ApiRoot.PathPrefix("/plugins").Subrouter() + api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() + + api.InitTeamLocal() + api.InitChannelLocal() + api.InitLicenseLocal() + api.InitConfigLocal() + api.InitPluginLocal() + + root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404)) + + return api +} + func (api *API) Handle404(w http.ResponseWriter, r *http.Request) { web.Handle404(api.ConfigService, w, r) } diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 01ad3b4f51..fa0aa18a51 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -55,6 +55,8 @@ type TestHelper struct { SystemAdminUser *model.User tempWorkspace string + LocalClient *model.Client4 + IncludeCacheLayer bool } @@ -78,6 +80,8 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent config := memoryStore.Get() *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + config.ServiceSettings.EnableLocalMode = model.NewBool(true) + *config.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock") if updateConfig != nil { updateConfig(config) } @@ -118,13 +122,13 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent }) prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) - serverErr := th.Server.Start() - if serverErr != nil { - panic(serverErr) + if err := th.Server.Start(); err != nil { + panic(err) } th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) Init(th.Server, th.Server.AppOptions, th.App.Srv().Router) + InitLocal(th.Server, th.Server.AppOptions, th.App.Srv().LocalRouter) web.New(th.Server, th.Server.AppOptions, th.App.Srv().Router) wsapi.Init(th.App, th.App.Srv().WebSocketRouter) th.App.DoAppMigrations() @@ -149,6 +153,8 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent th.Client = th.CreateClient() th.SystemAdminClient = th.CreateClient() + th.LocalClient = th.CreateLocalClient(*config.ServiceSettings.LocalModeSocketLocation) + if th.tempWorkspace == "" { th.tempWorkspace = tempWorkspace } @@ -343,6 +349,22 @@ func (me *TestHelper) CreateClient() *model.Client4 { return model.NewAPIv4Client(fmt.Sprintf("http://localhost:%v", me.App.Srv().ListenAddr.Port)) } +// ToDo: maybe move this to NewAPIv4SocketClient and reuse it in mmctl +func (me *TestHelper) CreateLocalClient(socketPath string) *model.Client4 { + httpClient := &http.Client{ + Transport: &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, + } + + return &model.Client4{ + ApiUrl: "http://_" + model.API_URL_SUFFIX, + HttpClient: httpClient, + } +} + func (me *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, *model.AppError) { return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", me.App.Srv().ListenAddr.Port), me.Client.AuthToken) } @@ -671,6 +693,46 @@ func (me *TestHelper) CreateGroup() *model.Group { return group } +// TestForSystemAdminAndLocal runs a test function for both +// SystemAdmin and Local clients. Several endpoints work in the same +// way when used by a fully privileged user and through the local +// mode, so this helper facilitates checking both +func (me *TestHelper) TestForSystemAdminAndLocal(t *testing.T, f func(*testing.T, *model.Client4), name ...string) { + var testName string + if len(name) > 0 { + testName = name[0] + "/" + } + + t.Run(testName+"SystemAdminClient", func(t *testing.T) { + f(t, me.SystemAdminClient) + }) + + t.Run(testName+"LocalClient", func(t *testing.T) { + f(t, me.LocalClient) + }) +} + +// TestForAllClients runs a test function for all the clients +// registered in the TestHelper +func (me *TestHelper) TestForAllClients(t *testing.T, f func(*testing.T, *model.Client4), name ...string) { + var testName string + if len(name) > 0 { + testName = name[0] + "/" + } + + t.Run(testName+"Client", func(t *testing.T) { + f(t, me.Client) + }) + + t.Run(testName+"SystemAdminClient", func(t *testing.T) { + f(t, me.SystemAdminClient) + }) + + t.Run(testName+"LocalClient", func(t *testing.T) { + f(t, me.LocalClient) + }) +} + func GenerateTestUsername() string { return "fakeuser" + model.NewRandomString(10) } diff --git a/api4/channel_local.go b/api4/channel_local.go new file mode 100644 index 0000000000..9920440c93 --- /dev/null +++ b/api4/channel_local.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "net/http" + + "github.com/mattermost/mattermost-server/v5/audit" + "github.com/mattermost/mattermost-server/v5/model" +) + +func (api *API) InitChannelLocal() { + api.BaseRoutes.Channels.Handle("", api.ApiLocal(getAllChannels)).Methods("GET") + api.BaseRoutes.Channels.Handle("", api.ApiLocal(localCreateChannel)).Methods("POST") +} + +func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) { + channel := model.ChannelFromJson(r.Body) + if channel == nil { + c.SetInvalidParam("channel") + return + } + + auditRec := c.MakeAuditRecord("localCreateChannel", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("channel", channel) + + sc, err := c.App.CreateChannel(channel, false) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + auditRec.AddMeta("channel", sc) // overwrite meta + c.LogAudit("name=" + channel.Name) + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(sc.ToJson())) +} diff --git a/api4/channel_test.go b/api4/channel_test.go index 245826c1e9..a2c079d612 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -106,13 +106,15 @@ func TestCreateChannel(t *testing.T) { _, resp = Client.CreateChannel(private) CheckNoError(t, resp) - channel.Name = GenerateTestChannelName() - _, resp = th.SystemAdminClient.CreateChannel(channel) - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + channel.Name = GenerateTestChannelName() + _, resp = client.CreateChannel(channel) + CheckNoError(t, resp) - private.Name = GenerateTestChannelName() - _, resp = th.SystemAdminClient.CreateChannel(private) - CheckNoError(t, resp) + private.Name = GenerateTestChannelName() + _, resp = client.CreateChannel(private) + CheckNoError(t, resp) + }) // Test posting Garbage r, err := Client.DoApiPost("/channels", "garbage") @@ -901,28 +903,30 @@ func TestGetAllChannels(t *testing.T) { defer th.TearDown() Client := th.Client - channels, resp := th.SystemAdminClient.GetAllChannels(0, 20, "") - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + channels, resp := client.GetAllChannels(0, 20, "") + CheckNoError(t, resp) - // At least, all the not-deleted channels created during the InitBasic - require.True(t, len(*channels) >= 3) - for _, c := range *channels { - require.NotEqual(t, c.TeamId, "") - } + // At least, all the not-deleted channels created during the InitBasic + require.True(t, len(*channels) >= 3) + for _, c := range *channels { + require.NotEqual(t, c.TeamId, "") + } - channels, resp = th.SystemAdminClient.GetAllChannels(0, 10, "") - CheckNoError(t, resp) - require.True(t, len(*channels) >= 3) + channels, resp = client.GetAllChannels(0, 10, "") + CheckNoError(t, resp) + require.True(t, len(*channels) >= 3) - channels, resp = th.SystemAdminClient.GetAllChannels(1, 1, "") - CheckNoError(t, resp) - require.Len(t, *channels, 1) + channels, resp = client.GetAllChannels(1, 1, "") + CheckNoError(t, resp) + require.Len(t, *channels, 1) - channels, resp = th.SystemAdminClient.GetAllChannels(10000, 10000, "") - CheckNoError(t, resp) - require.Empty(t, *channels) + channels, resp = client.GetAllChannels(10000, 10000, "") + CheckNoError(t, resp) + require.Empty(t, *channels) + }) - _, resp = Client.GetAllChannels(0, 20, "") + _, resp := Client.GetAllChannels(0, 20, "") CheckForbiddenStatus(t, resp) } diff --git a/api4/config_local.go b/api4/config_local.go new file mode 100644 index 0000000000..b01dee97df --- /dev/null +++ b/api4/config_local.go @@ -0,0 +1,112 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "net/http" + "reflect" + + "github.com/mattermost/mattermost-server/v5/audit" + "github.com/mattermost/mattermost-server/v5/config" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/utils" +) + +func (api *API) InitConfigLocal() { + api.BaseRoutes.ApiRoot.Handle("/config", api.ApiLocal(getConfig)).Methods("GET") + api.BaseRoutes.ApiRoot.Handle("/config", api.ApiLocal(localUpdateConfig)).Methods("PUT") + api.BaseRoutes.ApiRoot.Handle("/config/patch", api.ApiLocal(localPatchConfig)).Methods("PUT") +} + +func localUpdateConfig(c *Context, w http.ResponseWriter, r *http.Request) { + cfg := model.ConfigFromJson(r.Body) + if cfg == nil { + c.SetInvalidParam("config") + return + } + + auditRec := c.MakeAuditRecord("localUpdateConfig", audit.Fail) + defer c.LogAuditRec(auditRec) + + cfg.SetDefaults() + + appCfg := c.App.Config() + + // Do not allow plugin uploads to be toggled through the API + cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads + + // Do not allow certificates to be changed through the API + cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles + + c.App.HandleMessageExportConfig(cfg, appCfg) + + err := cfg.IsValid() + if err != nil { + c.Err = err + return + } + + err = c.App.SaveConfig(cfg, true) + if err != nil { + c.Err = err + return + } + + cfg = c.App.GetSanitizedConfig() + + auditRec.Success() + c.LogAudit("updateConfig") + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Write([]byte(cfg.ToJson())) +} + +func localPatchConfig(c *Context, w http.ResponseWriter, r *http.Request) { + cfg := model.ConfigFromJson(r.Body) + if cfg == nil { + c.SetInvalidParam("config") + return + } + + auditRec := c.MakeAuditRecord("localPatchConfig", audit.Fail) + defer c.LogAuditRec(auditRec) + + appCfg := c.App.Config() + filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool { + return true + } + + // Do not allow plugin uploads to be toggled through the API + cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads + + if cfg.MessageExportSettings.EnableExport != nil { + c.App.HandleMessageExportConfig(cfg, appCfg) + } + + updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{ + StructFieldFilter: filterFn, + }) + + if mergeErr != nil { + c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, mergeErr.Error(), http.StatusInternalServerError) + return + } + + err := updatedCfg.IsValid() + if err != nil { + c.Err = err + return + } + + err = c.App.SaveConfig(updatedCfg, true) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Write([]byte(c.App.GetSanitizedConfig().ToJson())) +} diff --git a/api4/config_test.go b/api4/config_test.go index ac41d52e29..76b888d70b 100644 --- a/api4/config_test.go +++ b/api4/config_test.go @@ -22,33 +22,35 @@ func TestGetConfig(t *testing.T) { _, resp := Client.GetConfig() CheckForbiddenStatus(t, resp) - cfg, resp := th.SystemAdminClient.GetConfig() - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + cfg, resp := client.GetConfig() + CheckNoError(t, resp) - require.NotEqual(t, "", cfg.TeamSettings.SiteName) + require.NotEqual(t, "", cfg.TeamSettings.SiteName) - if *cfg.LdapSettings.BindPassword != model.FAKE_SETTING && len(*cfg.LdapSettings.BindPassword) != 0 { - require.FailNow(t, "did not sanitize properly") - } - require.Equal(t, model.FAKE_SETTING, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly") + if *cfg.LdapSettings.BindPassword != model.FAKE_SETTING && len(*cfg.LdapSettings.BindPassword) != 0 { + require.FailNow(t, "did not sanitize properly") + } + require.Equal(t, model.FAKE_SETTING, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly") - if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FAKE_SETTING && len(*cfg.FileSettings.AmazonS3SecretAccessKey) != 0 { - require.FailNow(t, "did not sanitize properly") - } - if *cfg.EmailSettings.SMTPPassword != model.FAKE_SETTING && len(*cfg.EmailSettings.SMTPPassword) != 0 { - require.FailNow(t, "did not sanitize properly") - } - if *cfg.GitLabSettings.Secret != model.FAKE_SETTING && len(*cfg.GitLabSettings.Secret) != 0 { - require.FailNow(t, "did not sanitize properly") - } - require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.DataSource, "did not sanitize properly") - require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly") - if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceReplicas) != 0 { - require.FailNow(t, "did not sanitize properly") - } - if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 { - require.FailNow(t, "did not sanitize properly") - } + if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FAKE_SETTING && len(*cfg.FileSettings.AmazonS3SecretAccessKey) != 0 { + require.FailNow(t, "did not sanitize properly") + } + if *cfg.EmailSettings.SMTPPassword != model.FAKE_SETTING && len(*cfg.EmailSettings.SMTPPassword) != 0 { + require.FailNow(t, "did not sanitize properly") + } + if *cfg.GitLabSettings.Secret != model.FAKE_SETTING && len(*cfg.GitLabSettings.Secret) != 0 { + require.FailNow(t, "did not sanitize properly") + } + require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.DataSource, "did not sanitize properly") + require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly") + if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceReplicas) != 0 { + require.FailNow(t, "did not sanitize properly") + } + if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 { + require.FailNow(t, "did not sanitize properly") + } + }) } func TestReloadConfig(t *testing.T) { @@ -88,66 +90,68 @@ func TestUpdateConfig(t *testing.T) { _, resp = Client.UpdateConfig(cfg) CheckForbiddenStatus(t, resp) - SiteName := th.App.Config().TeamSettings.SiteName + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + SiteName := th.App.Config().TeamSettings.SiteName - *cfg.TeamSettings.SiteName = "MyFancyName" - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) + *cfg.TeamSettings.SiteName = "MyFancyName" + cfg, resp = client.UpdateConfig(cfg) + CheckNoError(t, resp) - require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName") + require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName") - //Revert the change - cfg.TeamSettings.SiteName = SiteName - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) - - require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName") - - t.Run("Should set defaults for missing fields", func(t *testing.T) { - _, appErr := th.SystemAdminClient.DoApiPut(th.SystemAdminClient.GetConfigRoute(), `{"ServiceSettings":{}}`) - require.Nil(t, appErr) - }) - - t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) { //Revert the change - badcfg := cfg.Clone() - badcfg.PasswordSettings.MinimumLength = model.NewInt(4) - badcfg.PasswordSettings.MinimumLength = model.NewInt(4) - _, resp = th.SystemAdminClient.UpdateConfig(badcfg) - CheckBadRequestStatus(t, resp) - CheckErrorMessage(t, resp, "model.config.is_valid.password_length.app_error") - }) - - t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) { - oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads - *cfg.PluginSettings.EnableUploads = !oldEnableUploads - - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) + cfg.TeamSettings.SiteName = SiteName + cfg, resp = client.UpdateConfig(cfg) CheckNoError(t, resp) - assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads) - assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads) - cfg.PluginSettings.EnableUploads = nil - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) - assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads) - assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads) - }) + require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName") - t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) { - oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles - cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature") + t.Run("Should set defaults for missing fields", func(t *testing.T) { + _, appErr := th.SystemAdminClient.DoApiPut(th.SystemAdminClient.GetConfigRoute(), `{"ServiceSettings":{}}`) + require.Nil(t, appErr) + }) - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) - assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles) - assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles) + t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) { + //Revert the change + badcfg := cfg.Clone() + badcfg.PasswordSettings.MinimumLength = model.NewInt(4) + badcfg.PasswordSettings.MinimumLength = model.NewInt(4) + _, resp = client.UpdateConfig(badcfg) + CheckBadRequestStatus(t, resp) + CheckErrorMessage(t, resp, "model.config.is_valid.password_length.app_error") + }) - cfg.PluginSettings.SignaturePublicKeyFiles = nil - cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) - assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles) - assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles) + t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) { + oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads + *cfg.PluginSettings.EnableUploads = !oldEnableUploads + + cfg, resp = client.UpdateConfig(cfg) + CheckNoError(t, resp) + assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads) + assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads) + + cfg.PluginSettings.EnableUploads = nil + cfg, resp = client.UpdateConfig(cfg) + CheckNoError(t, resp) + assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads) + assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads) + }) + + t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) { + oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles + cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature") + + cfg, resp = client.UpdateConfig(cfg) + CheckNoError(t, resp) + assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles) + assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles) + + cfg.PluginSettings.SignaturePublicKeyFiles = nil + cfg, resp = client.UpdateConfig(cfg) + CheckNoError(t, resp) + assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles) + assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles) + }) }) } @@ -224,23 +228,40 @@ func TestUpdateConfigRestrictSystemAdmin(t *testing.T) { defer th.TearDown() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) - originalCfg, resp := th.SystemAdminClient.GetConfig() - CheckNoError(t, resp) + t.Run("Restrict flag should be honored for sysadmin", func(t *testing.T) { + originalCfg, resp := th.SystemAdminClient.GetConfig() + CheckNoError(t, resp) - cfg := originalCfg.Clone() - *cfg.TeamSettings.SiteName = "MyFancyName" // Allowed - *cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored + cfg := originalCfg.Clone() + *cfg.TeamSettings.SiteName = "MyFancyName" // Allowed + *cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored - returnedCfg, resp := th.SystemAdminClient.UpdateConfig(cfg) - CheckNoError(t, resp) + returnedCfg, resp := th.SystemAdminClient.UpdateConfig(cfg) + CheckNoError(t, resp) - require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName) - require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL) + require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName) + require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL) - actualCfg, resp := th.SystemAdminClient.GetConfig() - CheckNoError(t, resp) + actualCfg, resp := th.SystemAdminClient.GetConfig() + CheckNoError(t, resp) - require.Equal(t, returnedCfg, actualCfg) + require.Equal(t, returnedCfg, actualCfg) + }) + + t.Run("Restrict flag should be ignored by local mode", func(t *testing.T) { + originalCfg, resp := th.LocalClient.GetConfig() + CheckNoError(t, resp) + + cfg := originalCfg.Clone() + *cfg.TeamSettings.SiteName = "MyFancyName" // Allowed + *cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored + + returnedCfg, resp := th.LocalClient.UpdateConfig(cfg) + CheckNoError(t, resp) + + require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName) + require.Equal(t, "http://example.com", *returnedCfg.ServiceSettings.SiteURL) + }) } func TestGetEnvironmentConfig(t *testing.T) { @@ -360,19 +381,18 @@ func TestGetOldClientConfig(t *testing.T) { func TestPatchConfig(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - client := th.Client t.Run("config is missing", func(t *testing.T) { - _, response := client.PatchConfig(nil) + _, response := th.Client.PatchConfig(nil) CheckBadRequestStatus(t, response) }) t.Run("user is not system admin", func(t *testing.T) { - _, response := client.PatchConfig(&model.Config{}) + _, response := th.Client.PatchConfig(&model.Config{}) CheckForbiddenStatus(t, response) }) - t.Run("should not update the restricted fields when restrict toggle is on", func(t *testing.T) { + t.Run("should not update the restricted fields when restrict toggle is on for sysadmin", func(t *testing.T) { *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true config := model.Config{LogSettings: model.LogSettings{ @@ -384,72 +404,94 @@ func TestPatchConfig(t *testing.T) { assert.Equal(t, "DEBUG", *updatedConfig.LogSettings.ConsoleLevel) }) - t.Run("check if config is valid", func(t *testing.T) { - config := model.Config{PasswordSettings: model.PasswordSettings{ - MinimumLength: model.NewInt(4), - }} + t.Run("should not bypass the restrict toggle if local client", func(t *testing.T) { + *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true - _, response := th.SystemAdminClient.PatchConfig(&config) - - assert.Equal(t, http.StatusBadRequest, response.StatusCode) - assert.NotNil(t, response.Error) - assert.Equal(t, "model.config.is_valid.password_length.app_error", response.Error.Id) - }) - - t.Run("should patch the config", func(t *testing.T) { - *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false - th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} }) - - oldConfig, _ := th.SystemAdminClient.GetConfig() - - assert.False(t, *oldConfig.PasswordSettings.Lowercase) - assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength) - assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel) - assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable) - - states := make(map[string]*model.PluginState) - states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewBool(false)} - config := model.Config{PasswordSettings: model.PasswordSettings{ - Lowercase: model.NewBool(true), - MinimumLength: model.NewInt(15), - }, LogSettings: model.LogSettings{ + config := model.Config{LogSettings: model.LogSettings{ ConsoleLevel: model.NewString("INFO"), - }, - TeamSettings: model.TeamSettings{ - ExperimentalDefaultChannels: []string{"another-channel"}, - }, - PluginSettings: model.PluginSettings{ - PluginStates: states, - }, - } + }} - _, response := th.SystemAdminClient.PatchConfig(&config) + oldConfig, _ := th.LocalClient.GetConfig() + updatedConfig, _ := th.LocalClient.PatchConfig(&config) - updatedConfig, _ := th.SystemAdminClient.GetConfig() - assert.True(t, *updatedConfig.PasswordSettings.Lowercase) assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel) - assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels) - assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable) - assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control")) + // reset the config + _, resp := th.LocalClient.UpdateConfig(oldConfig) + CheckNoError(t, resp) }) - t.Run("should sanitize config", func(t *testing.T) { - config := model.Config{PasswordSettings: model.PasswordSettings{ - Symbol: model.NewBool(true), - }} + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + t.Run("check if config is valid", func(t *testing.T) { + config := model.Config{PasswordSettings: model.PasswordSettings{ + MinimumLength: model.NewInt(4), + }} - updatedConfig, _ := th.SystemAdminClient.PatchConfig(&config) + _, response := client.PatchConfig(&config) - assert.Equal(t, model.FAKE_SETTING, *updatedConfig.SqlSettings.DataSource) - }) + assert.Equal(t, http.StatusBadRequest, response.StatusCode) + assert.NotNil(t, response.Error) + assert.Equal(t, "model.config.is_valid.password_length.app_error", response.Error.Id) + }) - t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) { - config := model.Config{PluginSettings: model.PluginSettings{ - EnableUploads: model.NewBool(true), - }} + t.Run("should patch the config", func(t *testing.T) { + *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false + th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} }) - updatedConfig, _ := th.SystemAdminClient.PatchConfig(&config) + oldConfig, _ := client.GetConfig() - assert.Equal(t, false, *updatedConfig.PluginSettings.EnableUploads) + assert.False(t, *oldConfig.PasswordSettings.Lowercase) + assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength) + assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel) + assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable) + + states := make(map[string]*model.PluginState) + states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewBool(false)} + config := model.Config{PasswordSettings: model.PasswordSettings{ + Lowercase: model.NewBool(true), + MinimumLength: model.NewInt(15), + }, LogSettings: model.LogSettings{ + ConsoleLevel: model.NewString("INFO"), + }, + TeamSettings: model.TeamSettings{ + ExperimentalDefaultChannels: []string{"another-channel"}, + }, + PluginSettings: model.PluginSettings{ + PluginStates: states, + }, + } + + _, response := client.PatchConfig(&config) + + updatedConfig, _ := client.GetConfig() + assert.True(t, *updatedConfig.PasswordSettings.Lowercase) + assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel) + assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels) + assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable) + assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control")) + + // reset the config + _, resp := client.UpdateConfig(oldConfig) + CheckNoError(t, resp) + }) + + t.Run("should sanitize config", func(t *testing.T) { + config := model.Config{PasswordSettings: model.PasswordSettings{ + Symbol: model.NewBool(true), + }} + + updatedConfig, _ := client.PatchConfig(&config) + + assert.Equal(t, model.FAKE_SETTING, *updatedConfig.SqlSettings.DataSource) + }) + + t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) { + config := model.Config{PluginSettings: model.PluginSettings{ + EnableUploads: model.NewBool(true), + }} + + updatedConfig, _ := client.PatchConfig(&config) + + assert.Equal(t, false, *updatedConfig.PluginSettings.EnableUploads) + }) }) } diff --git a/api4/handlers.go b/api4/handlers.go index 17486b0228..9a1a822f0e 100644 --- a/api4/handlers.go +++ b/api4/handlers.go @@ -23,6 +23,7 @@ func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) TrustRequester: false, RequireMfa: false, IsStatic: false, + IsLocal: false, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -41,6 +42,7 @@ func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.R TrustRequester: false, RequireMfa: true, IsStatic: false, + IsLocal: false, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -61,6 +63,7 @@ func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *htt TrustRequester: false, RequireMfa: false, IsStatic: false, + IsLocal: false, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -81,6 +84,7 @@ func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, * TrustRequester: true, RequireMfa: false, IsStatic: false, + IsLocal: false, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -100,6 +104,7 @@ func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseW TrustRequester: true, RequireMfa: true, IsStatic: false, + IsLocal: false, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -119,6 +124,7 @@ func (api *API) ApiSessionRequiredDisableWhenBusy(h func(*Context, http.Response TrustRequester: false, RequireMfa: false, IsStatic: false, + IsLocal: false, DisableWhenBusy: true, } if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { @@ -127,3 +133,25 @@ func (api *API) ApiSessionRequiredDisableWhenBusy(h func(*Context, http.Response return handler } + +// ApiLocal provides a handler for API endpoints to be used in local +// mode, this is, through a UNIX socket and without an authenticated +// session, but with one that has no user set and no permission +// restrictions +func (api *API) ApiLocal(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + handler := &web.Handler{ + GetGlobalAppOptions: api.GetGlobalAppOptions, + HandleFunc: h, + HandlerName: web.GetHandlerName(h), + RequireSession: false, + TrustRequester: false, + RequireMfa: false, + IsStatic: false, + IsLocal: true, + } + + if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { + return gziphandler.GzipHandler(handler) + } + return handler +} diff --git a/api4/license_local.go b/api4/license_local.go new file mode 100644 index 0000000000..b4d5b08a2a --- /dev/null +++ b/api4/license_local.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "bytes" + "io" + "net/http" + + "github.com/mattermost/mattermost-server/v5/audit" + "github.com/mattermost/mattermost-server/v5/model" +) + +func (api *API) InitLicenseLocal() { + api.BaseRoutes.ApiRoot.Handle("/license", api.ApiLocal(localAddLicense)).Methods("POST") + api.BaseRoutes.ApiRoot.Handle("/license", api.ApiLocal(localRemoveLicense)).Methods("DELETE") +} + +func localAddLicense(c *Context, w http.ResponseWriter, r *http.Request) { + auditRec := c.MakeAuditRecord("localAddLicense", audit.Fail) + defer c.LogAuditRec(auditRec) + c.LogAudit("attempt") + + err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + m := r.MultipartForm + + fileArray, ok := m.File["license"] + if !ok { + c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest) + return + } + + if len(fileArray) <= 0 { + c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest) + return + } + + fileData := fileArray[0] + auditRec.AddMeta("filename", fileData.Filename) + + file, err := fileData.Open() + if err != nil { + c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest) + return + } + defer file.Close() + + buf := bytes.NewBuffer(nil) + io.Copy(buf, file) + + license, appErr := c.App.SaveLicense(buf.Bytes()) + if appErr != nil { + if appErr.Id == model.EXPIRED_LICENSE_ERROR { + c.LogAudit("failed - expired or non-started license") + } else if appErr.Id == model.INVALID_LICENSE_ERROR { + c.LogAudit("failed - invalid license") + } else { + c.LogAudit("failed - unable to save license") + } + c.Err = appErr + return + } + + if *c.App.Config().JobSettings.RunJobs { + c.App.Srv().Jobs.Workers = c.App.Srv().Jobs.InitWorkers() + c.App.Srv().Jobs.StartWorkers() + } + + auditRec.Success() + c.LogAudit("success") + + w.Write([]byte(license.ToJson())) +} + +func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) { + auditRec := c.MakeAuditRecord("localRemoveLicense", audit.Fail) + defer c.LogAuditRec(auditRec) + c.LogAudit("attempt") + + if err := c.App.RemoveLicense(); err != nil { + c.Err = err + return + } + + auditRec.Success() + c.LogAudit("success") + + ReturnStatusOK(w) +} diff --git a/api4/license_test.go b/api4/license_test.go index 87409fbed0..dbb23b3306 100644 --- a/api4/license_test.go +++ b/api4/license_test.go @@ -47,6 +47,7 @@ func TestUploadLicenseFile(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() Client := th.Client + LocalClient := th.LocalClient t.Run("as system user", func(t *testing.T) { ok, resp := Client.UploadLicenseFile([]byte{}) @@ -54,11 +55,11 @@ func TestUploadLicenseFile(t *testing.T) { require.False(t, ok) }) - t.Run("as system admin user", func(t *testing.T) { - ok, resp := th.SystemAdminClient.UploadLicenseFile([]byte{}) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { + ok, resp := c.UploadLicenseFile([]byte{}) CheckBadRequestStatus(t, resp) require.False(t, ok) - }) + }, "as system admin user") t.Run("as restricted system admin user", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) @@ -67,12 +68,20 @@ func TestUploadLicenseFile(t *testing.T) { CheckForbiddenStatus(t, resp) require.False(t, ok) }) + + t.Run("restricted admin setting not honoured through local client", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) + ok, resp := LocalClient.UploadLicenseFile([]byte{}) + CheckBadRequestStatus(t, resp) + require.False(t, ok) + }) } func TestRemoveLicenseFile(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() Client := th.Client + LocalClient := th.LocalClient t.Run("as system user", func(t *testing.T) { ok, resp := Client.RemoveLicenseFile() @@ -80,11 +89,11 @@ func TestRemoveLicenseFile(t *testing.T) { require.False(t, ok) }) - t.Run("as system admin user", func(t *testing.T) { - ok, resp := th.SystemAdminClient.RemoveLicenseFile() + th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { + ok, resp := c.RemoveLicenseFile() CheckNoError(t, resp) require.True(t, ok) - }) + }, "as system admin user") t.Run("as restricted system admin user", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) @@ -93,4 +102,12 @@ func TestRemoveLicenseFile(t *testing.T) { CheckForbiddenStatus(t, resp) require.False(t, ok) }) + + t.Run("restricted admin setting not honoured through local client", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) + + ok, resp := LocalClient.RemoveLicenseFile() + CheckNoError(t, resp) + require.True(t, ok) + }) } diff --git a/api4/plugin_local.go b/api4/plugin_local.go new file mode 100644 index 0000000000..b4c68306eb --- /dev/null +++ b/api4/plugin_local.go @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +func (api *API) InitPluginLocal() { + api.BaseRoutes.Plugins.Handle("", api.ApiLocal(uploadPlugin)).Methods("POST") + api.BaseRoutes.Plugins.Handle("", api.ApiLocal(getPlugins)).Methods("GET") + api.BaseRoutes.Plugins.Handle("/install_from_url", api.ApiLocal(installPluginFromUrl)).Methods("POST") + api.BaseRoutes.Plugin.Handle("", api.ApiLocal(removePlugin)).Methods("DELETE") + api.BaseRoutes.Plugin.Handle("/enable", api.ApiLocal(enablePlugin)).Methods("POST") + api.BaseRoutes.Plugin.Handle("/disable", api.ApiLocal(disablePlugin)).Methods("POST") +} diff --git a/api4/plugin_test.go b/api4/plugin_test.go index b6f21be45e..a99d79f519 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -33,241 +33,243 @@ func TestPlugin(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - statesJson, err := json.Marshal(th.App.Config().PluginSettings.PluginStates) - require.Nil(t, err) - states := map[string]*model.PluginState{} - json.Unmarshal(statesJson, &states) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.EnableUploads = true - *cfg.PluginSettings.AllowInsecureDownloadUrl = true - }) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + statesJson, err := json.Marshal(th.App.Config().PluginSettings.PluginStates) + require.Nil(t, err) + states := map[string]*model.PluginState{} + json.Unmarshal(statesJson, &states) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableUploads = true + *cfg.PluginSettings.AllowInsecureDownloadUrl = true + }) - path, _ := fileutils.FindDir("tests") - tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) - require.NoError(t, err) + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) - // Install from URL - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.WriteHeader(http.StatusOK) - res.Write(tarData) - })) - defer func() { testServer.Close() }() - - url := testServer.URL - - manifest, resp := th.SystemAdminClient.InstallPluginFromUrl(url, false) - CheckNoError(t, resp) - assert.Equal(t, "testplugin", manifest.Id) - - _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) - CheckBadRequestStatus(t, resp) - - manifest, resp = th.SystemAdminClient.InstallPluginFromUrl(url, true) - CheckNoError(t, resp) - assert.Equal(t, "testplugin", manifest.Id) - - // Stored in File Store: Install Plugin from URL case - pluginStored, err := th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz") - assert.Nil(t, err) - assert.True(t, pluginStored) - - ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNoError(t, resp) - require.True(t, ok) - - t.Run("install plugin from URL with slow response time", func(t *testing.T) { - if testing.Short() { - t.Skip("skipping test to install plugin from a slow response server") - } - - // Install from URL - slow server to simulate longer bundle download times - slowTestServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - time.Sleep(60 * time.Second) // Wait longer than the previous default 30 seconds timeout + // Install from URL + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) res.Write(tarData) })) - defer func() { slowTestServer.Close() }() + defer func() { testServer.Close() }() - manifest, resp = th.SystemAdminClient.InstallPluginFromUrl(slowTestServer.URL, true) + url := testServer.URL + + manifest, resp := client.InstallPluginFromUrl(url, false) CheckNoError(t, resp) assert.Equal(t, "testplugin", manifest.Id) + + _, resp = client.InstallPluginFromUrl(url, false) + CheckBadRequestStatus(t, resp) + + manifest, resp = client.InstallPluginFromUrl(url, true) + CheckNoError(t, resp) + assert.Equal(t, "testplugin", manifest.Id) + + // Stored in File Store: Install Plugin from URL case + pluginStored, err := th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz") + assert.Nil(t, err) + assert.True(t, pluginStored) + + ok, resp := client.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + require.True(t, ok) + + t.Run("install plugin from URL with slow response time", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test to install plugin from a slow response server") + } + + // Install from URL - slow server to simulate longer bundle download times + slowTestServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + time.Sleep(60 * time.Second) // Wait longer than the previous default 30 seconds timeout + res.WriteHeader(http.StatusOK) + res.Write(tarData) + })) + defer func() { slowTestServer.Close() }() + + manifest, resp = client.InstallPluginFromUrl(slowTestServer.URL, true) + CheckNoError(t, resp) + assert.Equal(t, "testplugin", manifest.Id) + }) + + th.App.RemovePlugin(manifest.Id) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) + + _, resp = client.InstallPluginFromUrl(url, false) + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + + _, resp = th.Client.InstallPluginFromUrl(url, false) + CheckForbiddenStatus(t, resp) + + _, resp = client.InstallPluginFromUrl("http://nodata", false) + CheckBadRequestStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.AllowInsecureDownloadUrl = false }) + + _, resp = client.InstallPluginFromUrl(url, false) + CheckBadRequestStatus(t, resp) + + // Successful upload + manifest, resp = client.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true }) + + manifest, resp = client.UploadPluginForced(bytes.NewReader(tarData)) + defer os.RemoveAll("plugins/testplugin") + CheckNoError(t, resp) + + assert.Equal(t, "testplugin", manifest.Id) + + // Stored in File Store: Upload Plugin case + pluginStored, err = th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz") + assert.Nil(t, err) + assert.True(t, pluginStored) + + // Upload error cases + _, resp = client.UploadPlugin(bytes.NewReader([]byte("badfile"))) + CheckBadRequestStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) + _, resp = client.UploadPlugin(bytes.NewReader(tarData)) + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableUploads = false + }) + _, resp = client.UploadPlugin(bytes.NewReader(tarData)) + CheckNotImplementedStatus(t, resp) + + _, resp = client.InstallPluginFromUrl(url, false) + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true }) + _, resp = th.Client.UploadPlugin(bytes.NewReader(tarData)) + CheckForbiddenStatus(t, resp) + + // Successful gets + pluginsResp, resp := client.GetPlugins() + CheckNoError(t, resp) + + found := false + for _, m := range pluginsResp.Inactive { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) + + found = false + for _, m := range pluginsResp.Active { + if m.Id == manifest.Id { + found = true + } + } + + assert.False(t, found) + + // Successful activate + ok, resp = client.EnablePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + pluginsResp, resp = client.GetPlugins() + CheckNoError(t, resp) + + found = false + for _, m := range pluginsResp.Active { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) + + // Activate error case + ok, resp = client.EnablePlugin("junk") + CheckNotFoundStatus(t, resp) + assert.False(t, ok) + + ok, resp = client.EnablePlugin("JUNK") + CheckNotFoundStatus(t, resp) + assert.False(t, ok) + + // Successful deactivate + ok, resp = client.DisablePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + pluginsResp, resp = client.GetPlugins() + CheckNoError(t, resp) + + found = false + for _, m := range pluginsResp.Inactive { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) + + // Deactivate error case + ok, resp = client.DisablePlugin("junk") + CheckNotFoundStatus(t, resp) + assert.False(t, ok) + + // Get error cases + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) + _, resp = client.GetPlugins() + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + _, resp = th.Client.GetPlugins() + CheckForbiddenStatus(t, resp) + + // Successful webapp get + _, resp = client.EnablePlugin(manifest.Id) + CheckNoError(t, resp) + + manifests, resp := th.Client.GetWebappPlugins() + CheckNoError(t, resp) + + found = false + for _, m := range manifests { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) + + // Successful remove + ok, resp = client.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + // Remove error cases + ok, resp = client.RemovePlugin(manifest.Id) + CheckNotFoundStatus(t, resp) + assert.False(t, ok) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) + _, resp = client.RemovePlugin(manifest.Id) + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + _, resp = th.Client.RemovePlugin(manifest.Id) + CheckForbiddenStatus(t, resp) + + _, resp = client.RemovePlugin("bad.id") + CheckNotFoundStatus(t, resp) }) - - th.App.RemovePlugin(manifest.Id) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) - - _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) - CheckNotImplementedStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) - - _, resp = th.Client.InstallPluginFromUrl(url, false) - CheckForbiddenStatus(t, resp) - - _, resp = th.SystemAdminClient.InstallPluginFromUrl("http://nodata", false) - CheckBadRequestStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.AllowInsecureDownloadUrl = false }) - - _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) - CheckBadRequestStatus(t, resp) - - // Successful upload - manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) - CheckNoError(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true }) - - manifest, resp = th.SystemAdminClient.UploadPluginForced(bytes.NewReader(tarData)) - defer os.RemoveAll("plugins/testplugin") - CheckNoError(t, resp) - - assert.Equal(t, "testplugin", manifest.Id) - - // Stored in File Store: Upload Plugin case - pluginStored, err = th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz") - assert.Nil(t, err) - assert.True(t, pluginStored) - - // Upload error cases - _, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader([]byte("badfile"))) - CheckBadRequestStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) - _, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) - CheckNotImplementedStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.EnableUploads = false - }) - _, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) - CheckNotImplementedStatus(t, resp) - - _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) - CheckNotImplementedStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true }) - _, resp = th.Client.UploadPlugin(bytes.NewReader(tarData)) - CheckForbiddenStatus(t, resp) - - // Successful gets - pluginsResp, resp := th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - - found := false - for _, m := range pluginsResp.Inactive { - if m.Id == manifest.Id { - found = true - } - } - - assert.True(t, found) - - found = false - for _, m := range pluginsResp.Active { - if m.Id == manifest.Id { - found = true - } - } - - assert.False(t, found) - - // Successful activate - ok, resp = th.SystemAdminClient.EnablePlugin(manifest.Id) - CheckNoError(t, resp) - assert.True(t, ok) - - pluginsResp, resp = th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - - found = false - for _, m := range pluginsResp.Active { - if m.Id == manifest.Id { - found = true - } - } - - assert.True(t, found) - - // Activate error case - ok, resp = th.SystemAdminClient.EnablePlugin("junk") - CheckNotFoundStatus(t, resp) - assert.False(t, ok) - - ok, resp = th.SystemAdminClient.EnablePlugin("JUNK") - CheckNotFoundStatus(t, resp) - assert.False(t, ok) - - // Successful deactivate - ok, resp = th.SystemAdminClient.DisablePlugin(manifest.Id) - CheckNoError(t, resp) - assert.True(t, ok) - - pluginsResp, resp = th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - - found = false - for _, m := range pluginsResp.Inactive { - if m.Id == manifest.Id { - found = true - } - } - - assert.True(t, found) - - // Deactivate error case - ok, resp = th.SystemAdminClient.DisablePlugin("junk") - CheckNotFoundStatus(t, resp) - assert.False(t, ok) - - // Get error cases - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) - _, resp = th.SystemAdminClient.GetPlugins() - CheckNotImplementedStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) - _, resp = th.Client.GetPlugins() - CheckForbiddenStatus(t, resp) - - // Successful webapp get - _, resp = th.SystemAdminClient.EnablePlugin(manifest.Id) - CheckNoError(t, resp) - - manifests, resp := th.Client.GetWebappPlugins() - CheckNoError(t, resp) - - found = false - for _, m := range manifests { - if m.Id == manifest.Id { - found = true - } - } - - assert.True(t, found) - - // Successful remove - ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNoError(t, resp) - assert.True(t, ok) - - // Remove error cases - ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNotFoundStatus(t, resp) - assert.False(t, ok) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) - _, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNotImplementedStatus(t, resp) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) - _, resp = th.Client.RemovePlugin(manifest.Id) - CheckForbiddenStatus(t, resp) - - _, resp = th.SystemAdminClient.RemovePlugin("bad.id") - CheckNotFoundStatus(t, resp) } func TestNotifyClusterPluginEvent(t *testing.T) { @@ -387,80 +389,82 @@ func TestDisableOnRemove(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.EnableUploads = true - }) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableUploads = true + }) - // Upload - manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) - CheckNoError(t, resp) - require.Equal(t, "testplugin", manifest.Id) - - // Check initial status - pluginsResp, resp := th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - require.Empty(t, pluginsResp.Active) - require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ - Manifest: *manifest, - }}) - - // Enable plugin - ok, resp := th.SystemAdminClient.EnablePlugin(manifest.Id) - CheckNoError(t, resp) - require.True(t, ok) - - // Confirm enabled status - pluginsResp, resp = th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - require.Empty(t, pluginsResp.Inactive) - require.Equal(t, pluginsResp.Active, []*model.PluginInfo{{ - Manifest: *manifest, - }}) - - if tc.Upgrade { - // Upgrade - manifest, resp = th.SystemAdminClient.UploadPluginForced(bytes.NewReader(tarData)) + // Upload + manifest, resp := client.UploadPlugin(bytes.NewReader(tarData)) CheckNoError(t, resp) require.Equal(t, "testplugin", manifest.Id) - // Plugin should remain active - pluginsResp, resp = th.SystemAdminClient.GetPlugins() + // Check initial status + pluginsResp, resp := client.GetPlugins() + CheckNoError(t, resp) + require.Empty(t, pluginsResp.Active) + require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ + Manifest: *manifest, + }}) + + // Enable plugin + ok, resp := client.EnablePlugin(manifest.Id) + CheckNoError(t, resp) + require.True(t, ok) + + // Confirm enabled status + pluginsResp, resp = client.GetPlugins() CheckNoError(t, resp) require.Empty(t, pluginsResp.Inactive) require.Equal(t, pluginsResp.Active, []*model.PluginInfo{{ Manifest: *manifest, }}) - } - // Remove plugin - ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNoError(t, resp) - require.True(t, ok) + if tc.Upgrade { + // Upgrade + manifest, resp = client.UploadPluginForced(bytes.NewReader(tarData)) + CheckNoError(t, resp) + require.Equal(t, "testplugin", manifest.Id) - // Plugin should have no status - pluginsResp, resp = th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - require.Empty(t, pluginsResp.Inactive) - require.Empty(t, pluginsResp.Active) + // Plugin should remain active + pluginsResp, resp = client.GetPlugins() + CheckNoError(t, resp) + require.Empty(t, pluginsResp.Inactive) + require.Equal(t, pluginsResp.Active, []*model.PluginInfo{{ + Manifest: *manifest, + }}) + } - // Upload same plugin - manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) - CheckNoError(t, resp) - require.Equal(t, "testplugin", manifest.Id) + // Remove plugin + ok, resp = client.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + require.True(t, ok) - // Plugin should be inactive - pluginsResp, resp = th.SystemAdminClient.GetPlugins() - CheckNoError(t, resp) - require.Empty(t, pluginsResp.Active) - require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ - Manifest: *manifest, - }}) + // Plugin should have no status + pluginsResp, resp = client.GetPlugins() + CheckNoError(t, resp) + require.Empty(t, pluginsResp.Inactive) + require.Empty(t, pluginsResp.Active) - // Clean up - ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) - CheckNoError(t, resp) - require.True(t, ok) + // Upload same plugin + manifest, resp = client.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + require.Equal(t, "testplugin", manifest.Id) + + // Plugin should be inactive + pluginsResp, resp = client.GetPlugins() + CheckNoError(t, resp) + require.Empty(t, pluginsResp.Active) + require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ + Manifest: *manifest, + }}) + + // Clean up + ok, resp = client.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + require.True(t, ok) + }) }) } } diff --git a/api4/team_local.go b/api4/team_local.go new file mode 100644 index 0000000000..184e1eccd5 --- /dev/null +++ b/api4/team_local.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +func (api *API) InitTeamLocal() { + api.BaseRoutes.Teams.Handle("", api.ApiLocal(getAllTeams)).Methods("GET") +} diff --git a/app/authorization.go b/app/authorization.go index 8d47043433..74ea109a9a 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -16,6 +16,9 @@ func (a *App) MakePermissionError(permission *model.Permission) *model.AppError } func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool { + if session.IsUnrestricted() { + return true + } return a.RolesGrantPermission(session.GetUserRoles(), permission.Id) } diff --git a/app/server.go b/app/server.go index 6c6cbb282d..364998d6b3 100644 --- a/app/server.go +++ b/app/server.go @@ -55,6 +55,10 @@ type Server struct { // RootRouter is the starting point for all HTTP requests to the server. RootRouter *mux.Router + // LocalRouter is the starting point for all the local UNIX socket + // requests to the server + LocalRouter *mux.Router + // Router is the starting point for all web, api4 and ws requests to the server. It differs // from RootRouter only if the SiteURL contains a /subpath. Router *mux.Router @@ -64,6 +68,8 @@ type Server struct { RateLimiter *RateLimiter Busy *Busy + localModeServer *http.Server + didFinishListen chan struct{} goroutineCount int32 @@ -154,10 +160,12 @@ type Server struct { func NewServer(options ...Option) (*Server, error) { rootRouter := mux.NewRouter() + localRouter := mux.NewRouter() s := &Server{ goroutineExitSignal: make(chan struct{}, 1), RootRouter: rootRouter, + LocalRouter: localRouter, licenseListeners: map[string]func(*model.License, *model.License){}, clientConfig: make(map[string]string), } @@ -433,6 +441,8 @@ func (s *Server) Shutdown() error { } s.StopHTTPServer() + s.stopLocalModeServer() + s.WaitForGoroutines() if s.htmlTemplateWatcher != nil { @@ -712,9 +722,44 @@ func (s *Server) Start() error { close(s.didFinishListen) }() + if *s.Config().ServiceSettings.EnableLocalMode { + if err := s.startLocalModeServer(); err != nil { + mlog.Critical(err.Error()) + } + } + return nil } +func (s *Server) startLocalModeServer() error { + s.localModeServer = &http.Server{ + Handler: s.LocalRouter, + } + + socket := *s.configStore.Get().ServiceSettings.LocalModeSocketLocation + unixListener, err := net.Listen("unix", socket) + if err != nil { + return errors.Wrapf(err, utils.T("api.server.start_server.starting.critical"), err) + } + if err = os.Chmod(socket, 0600); err != nil { + return errors.Wrapf(err, utils.T("api.server.start_server.starting.critical"), err) + } + + go func() { + err = s.localModeServer.Serve(unixListener) + if err != nil && err != http.ErrServerClosed { + mlog.Critical("Error starting unix socket server", mlog.Err(err)) + } + }() + return nil +} + +func (s *Server) stopLocalModeServer() { + if s.localModeServer != nil { + s.localModeServer.Close() + } +} + func (a *App) OriginChecker() func(*http.Request) bool { if allowed := *a.Config().ServiceSettings.AllowCorsFrom; allowed != "" { if allowed != "*" { diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index 4dd296461b..63e86b34f7 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -75,6 +75,7 @@ func runServer(configStore config.Store, disableConfigWatch bool, usedPlatform b api := api4.Init(server, server.AppOptions, server.Router) wsapi.Init(server.FakeApp(), server.WebSocketRouter) web.New(server, server.AppOptions, server.Router) + api4.InitLocal(server, server.AppOptions, server.LocalRouter) serverErr := server.Start() if serverErr != nil { diff --git a/i18n/en.json b/i18n/en.json index a6db5105aa..28b5887d2a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1138,6 +1138,10 @@ "id": "api.context.invalid_url_param.app_error", "translation": "Invalid or missing {{.Name}} parameter in request URL." }, + { + "id": "api.context.local_origin_required.app_error", + "translation": "This endpoint requires a local request origin." + }, { "id": "api.context.mfa_required.app_error", "translation": "Multi-factor authentication is required on this server." diff --git a/model/config.go b/model/config.go index 8ab8697193..eaae7afe58 100644 --- a/model/config.go +++ b/model/config.go @@ -213,6 +213,8 @@ const ( OFFICE365_SETTINGS_DEFAULT_AUTH_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" OFFICE365_SETTINGS_DEFAULT_TOKEN_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/token" OFFICE365_SETTINGS_DEFAULT_USER_API_ENDPOINT = "https://graph.microsoft.com/v1.0/me" + + LOCAL_MODE_SOCKET_PATH = "/var/tmp/mattermost_local.socket" ) var ServerTLSSupportedCiphers = map[string]uint16{ @@ -329,6 +331,8 @@ type ServiceSettings struct { EnableBotAccountCreation *bool EnableSVGs *bool EnableLatex *bool + EnableLocalMode *bool + LocalModeSocketLocation *string } func (s *ServiceSettings) SetDefaults(isUpdate bool) { @@ -722,6 +726,14 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.EnableLatex = NewBool(false) } } + + if s.EnableLocalMode == nil { + s.EnableLocalMode = NewBool(false) + } + + if s.LocalModeSocketLocation == nil { + s.LocalModeSocketLocation = NewString(LOCAL_MODE_SOCKET_PATH) + } } type ClusterSettings struct { diff --git a/model/session.go b/model/session.go index a391774ba5..986f89381e 100644 --- a/model/session.go +++ b/model/session.go @@ -39,6 +39,13 @@ type Session struct { IsOAuth bool `json:"is_oauth"` Props StringMap `json:"props"` TeamMembers []*TeamMember `json:"team_members" db:"-"` + Local bool `json:"local" db:"-"` +} + +// Returns true if the session is unrestricted, which should grant it +// with all permissions. This is used for local mode sessions +func (me *Session) IsUnrestricted() bool { + return me.Local } func (me *Session) DeepCopy() *Session { diff --git a/web/handlers.go b/web/handlers.go index 4c09286504..effb391587 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -45,6 +45,7 @@ func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) h TrustRequester: false, RequireMfa: false, IsStatic: false, + IsLocal: false, } } @@ -74,6 +75,7 @@ type Handler struct { TrustRequester bool RequireMfa bool IsStatic bool + IsLocal bool DisableWhenBusy bool cspShaDirective string @@ -210,6 +212,17 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SetServerBusyError() } + if c.Err == nil && h.IsLocal { + // if the connection is local, RemoteAddr shouldn't have the + // shape IP:PORT (it will be "@" in Linux, for example) + isLocalOrigin := !strings.Contains(r.RemoteAddr, ":") + if *c.App.Config().ServiceSettings.EnableLocalMode && isLocalOrigin { + c.App.SetSession(&model.Session{Local: true}) + } else if !isLocalOrigin { + c.Err = model.NewAppError("", "api.context.local_origin_required.app_error", nil, "LocalOriginRequired", http.StatusUnauthorized) + } + } + if c.Err == nil { h.HandleFunc(c, w, r) } @@ -323,6 +336,7 @@ func (w *Web) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) h TrustRequester: false, RequireMfa: false, IsStatic: false, + IsLocal: false, } if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -342,6 +356,7 @@ func (w *Web) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *ht TrustRequester: true, RequireMfa: false, IsStatic: false, + IsLocal: false, } if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler) @@ -360,6 +375,7 @@ func (w *Web) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Req TrustRequester: false, RequireMfa: true, IsStatic: false, + IsLocal: false, } if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { return gziphandler.GzipHandler(handler)