diff options
author | George Goldberg <george@gberg.me> | 2018-02-13 13:35:52 +0000 |
---|---|---|
committer | George Goldberg <george@gberg.me> | 2018-02-13 13:46:01 +0000 |
commit | 5c101253c5987743cf1b3a8fe68814d748070622 (patch) | |
tree | e2632505d051e7d15d6daf974ed8ac92095f82fe | |
parent | b7fc3d7d35ca4dd16097715a66463392a1dfaf0a (diff) | |
parent | d88d2bc2ed3aefa68b5ed2942f493ae42bb40bfa (diff) | |
download | chat-5c101253c5987743cf1b3a8fe68814d748070622.tar.gz chat-5c101253c5987743cf1b3a8fe68814d748070622.tar.bz2 chat-5c101253c5987743cf1b3a8fe68814d748070622.zip |
Merge branch 'master' into advanced-permissions-phase-1
74 files changed, 939 insertions, 666 deletions
@@ -276,7 +276,7 @@ store-mocks: ## Creates mock files. GOPATH=$(shell go env GOPATH) $(shell go env GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.' update-jira-plugin: ## Updates Jira plugin. - go get github.com/jteeuwen/go-bindata/... + go get github.com/mattermost/go-bindata/... curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz $(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_darwin_amd64.go plugin.tar.gz curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz @@ -287,7 +287,7 @@ update-jira-plugin: ## Updates Jira plugin. gofmt -s -w ./app/plugin/jira update-zoom-plugin: ## Updates Zoom plugin. - go get github.com/jteeuwen/go-bindata/... + go get github.com/mattermost/go-bindata/... curl -s https://api.github.com/repos/mattermost/mattermost-plugin-zoom/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz $(shell go env GOPATH)/bin/go-bindata -pkg zoom -o app/plugin/zoom/plugin_darwin_amd64.go plugin.tar.gz curl -s https://api.github.com/repos/mattermost/mattermost-plugin-zoom/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz diff --git a/api/api.go b/api/api.go index 2d65bb216..70f36db85 100644 --- a/api/api.go +++ b/api/api.go @@ -109,7 +109,7 @@ func Init(a *app.App, root *mux.Router) *API { api.InitReaction() // 404 on any api route before web.go has a chance to serve it - root.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404)) + root.Handle("/api/{anything:.*}", http.HandlerFunc(api.Handle404)) a.InitEmailBatching() @@ -120,6 +120,10 @@ func Init(a *app.App, root *mux.Router) *API { return api } +func (api *API) Handle404(w http.ResponseWriter, r *http.Request) { + Handle404(api.App, w, r) +} + func ReturnStatusOK(w http.ResponseWriter) { m := make(map[string]string) m[model.STATUS] = model.STATUS_OK diff --git a/api/apitestlib.go b/api/apitestlib.go index dece29b89..6e2b8c045 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -119,9 +119,10 @@ func setupTestHelper(enterprise bool) *TestHelper { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true }) - utils.SetIsLicensed(enterprise) if enterprise { - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) + } else { + th.App.SetLicense(nil) } return th diff --git a/api/context.go b/api/context.go index b28a24731..a8ff2b694 100644 --- a/api/context.go +++ b/api/context.go @@ -229,7 +229,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if c.Err.StatusCode == http.StatusUnauthorized { http.Redirect(w, r, c.GetTeamURL()+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) } else { - utils.RenderWebError(c.Err, w, r) + utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) } } @@ -434,7 +434,7 @@ func IsApiCall(r *http.Request) bool { return strings.Index(r.URL.Path, "/api/") == 0 } -func Handle404(w http.ResponseWriter, r *http.Request) { +func Handle404(a *app.App, w http.ResponseWriter, r *http.Request) { err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound) l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)) @@ -444,7 +444,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) { err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'. Typo? are you missing a team_id or user_id as part of the url?" w.Write([]byte(err.ToJson())) } else { - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, a.AsymmetricSigningKey()) } } diff --git a/api/file.go b/api/file.go index 2d626304e..3b8984816 100644 --- a/api/file.go +++ b/api/file.go @@ -174,12 +174,12 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { if hash != correctHash { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+utils.T(c.Err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) return } } else { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+utils.T(c.Err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) return } diff --git a/api/license.go b/api/license.go index 8eb7803e1..432442ad6 100644 --- a/api/license.go +++ b/api/license.go @@ -9,7 +9,6 @@ import ( "net/http" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func (api *API) InitLicense() { @@ -83,7 +82,7 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) { func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) { useSanitizedLicense := !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) - etag := utils.GetClientLicenseEtag(useSanitizedLicense) + etag := c.App.GetClientLicenseEtag(useSanitizedLicense) if c.HandleEtag(etag, "Get Client License Config", w, r) { return } @@ -91,9 +90,9 @@ func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) var clientLicense map[string]string if useSanitizedLicense { - clientLicense = utils.ClientLicense() + clientLicense = c.App.ClientLicense() } else { - clientLicense = utils.GetSanitizedClientLicense() + clientLicense = c.App.GetSanitizedClientLicense() } w.Header().Set(model.HEADER_ETAG_SERVER, etag) diff --git a/api/license_test.go b/api/license_test.go index 50d73101d..47586151a 100644 --- a/api/license_test.go +++ b/api/license_test.go @@ -5,8 +5,6 @@ package api import ( "testing" - - "github.com/mattermost/mattermost-server/utils" ) func TestGetLicenceConfig(t *testing.T) { @@ -32,7 +30,7 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("cache should be empty") } - utils.SetClientLicense(map[string]string{"IsLicensed": "true"}) + th.App.SetClientLicense(map[string]string{"IsLicensed": "true"}) if cache_result, err := Client.GetClientLicenceConfig(result.Etag); err != nil { t.Fatal(err) @@ -40,7 +38,7 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("result should not be empty") } - utils.SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) + th.App.SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) if cache_result, err := Client.GetClientLicenceConfig(result.Etag); err != nil { t.Fatal(err) @@ -48,6 +46,6 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("result should not be empty") } - utils.SetClientLicense(map[string]string{"IsLicensed": "false"}) + th.App.SetClientLicense(map[string]string{"IsLicensed": "false"}) } } diff --git a/api/post_test.go b/api/post_test.go index b88c733db..7a2367312 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -160,18 +160,8 @@ func TestCreatePost(t *testing.T) { } } - isLicensed := utils.IsLicensed() - license := utils.License() - disableTownSquareReadOnly := th.App.Config().TeamSettings.ExperimentalTownSquareIsReadOnly - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = disableTownSquareReadOnly }) - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true }) - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) defaultChannel := store.Must(th.App.Srv.Store.Channel().GetByName(team.Id, model.DEFAULT_CHANNEL, true)).(*model.Channel) defaultPost := &model.Post{ @@ -400,6 +390,7 @@ func TestUpdatePost(t *testing.T) { defer func() { th.RestoreDefaultRolePermissions(defaultRolePermissions) }() + th.App.SetLicense(model.NewTestLicense()) th.AddPermissionToRole(model.PERMISSION_EDIT_POST.Id, model.CHANNEL_USER_ROLE_ID) @@ -470,17 +461,8 @@ func TestUpdatePost(t *testing.T) { } // Test licensed policy controls for edit post - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() - th.RemovePermissionFromRole(model.PERMISSION_EDIT_POST.Id, model.CHANNEL_USER_ROLE_ID) + post4 := &model.Post{ChannelId: channel1.Id, Message: "zz" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} rpost4, err := Client.CreatePost(post4) if err != nil { diff --git a/api/user.go b/api/user.go index 440ea5858..ad4f12ef3 100644 --- a/api/user.go +++ b/api/user.go @@ -299,9 +299,9 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { il.ClientCfg = c.App.ClientConfig() if c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - il.LicenseCfg = utils.ClientLicense() + il.LicenseCfg = c.App.ClientLicense() } else { - il.LicenseCfg = utils.GetSanitizedClientLicense() + il.LicenseCfg = c.App.GetSanitizedClientLicense() } w.Write([]byte(il.ToJson())) diff --git a/api/user_test.go b/api/user_test.go index 8d6aad22b..f65d7c45b 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1889,17 +1889,7 @@ func TestUpdateMfa(t *testing.T) { Client := th.BasicClient - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(false) - utils.SetLicense(&model.License{Features: &model.Features{}}) - if utils.License().Features.MFA == nil { - utils.License().Features.MFA = new(bool) - } + th.App.SetLicense(nil) team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := Client.CreateTeam(&team) @@ -1925,8 +1915,7 @@ func TestUpdateMfa(t *testing.T) { t.Fatal("should have failed - not licensed") } - utils.SetIsLicensed(true) - *utils.License().Features.MFA = true + th.App.SetLicense(model.NewTestLicense("mfa")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true }) if _, err := Client.UpdateMfa(true, "123456"); err == nil { diff --git a/api/webhook_test.go b/api/webhook_test.go index b6b754ad3..0b3073f83 100644 --- a/api/webhook_test.go +++ b/api/webhook_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func TestCreateIncomingHook(t *testing.T) { @@ -980,10 +979,6 @@ func TestIncomingWebhooks(t *testing.T) { user2 := th.CreateUser(Client) th.LinkUserToTeam(user2, team) - enableIncomingHooks := th.App.Config().ServiceSettings.EnableIncomingWebhooks - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks }) - }() th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = true }) hook := &model.IncomingWebhook{ChannelId: channel1.Id} @@ -1025,18 +1020,8 @@ func TestIncomingWebhooks(t *testing.T) { t.Fatal("should not have failed -- ExperimentalTownSquareIsReadOnly is false and it's not a read only channel") } - isLicensed := utils.IsLicensed() - license := utils.License() - disableTownSquareReadOnly := th.App.Config().TeamSettings.ExperimentalTownSquareIsReadOnly - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = disableTownSquareReadOnly }) - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true }) - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) if _, err := th.BasicClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL), "application/json"); err == nil { t.Fatal("should have failed -- ExperimentalTownSquareIsReadOnly is true and it's a read only channel") diff --git a/api4/api.go b/api4/api.go index 583a6041b..88526e4d3 100644 --- a/api4/api.go +++ b/api4/api.go @@ -76,6 +76,8 @@ type Routes struct { Compliance *mux.Router // 'api/v4/compliance' Cluster *mux.Router // 'api/v4/cluster' + Image *mux.Router // 'api/v4/image' + LDAP *mux.Router // 'api/v4/ldap' Elasticsearch *mux.Router // 'api/v4/elasticsearch' @@ -198,6 +200,8 @@ func Init(a *app.App, root *mux.Router, full bool) *API { api.BaseRoutes.Roles = api.BaseRoutes.ApiRoot.PathPrefix("/roles").Subrouter() + api.BaseRoutes.Image = api.BaseRoutes.ApiRoot.PathPrefix("/image").Subrouter() + api.InitUser() api.InitTeam() api.InitChannel() @@ -224,6 +228,7 @@ func Init(a *app.App, root *mux.Router, full bool) *API { api.InitOpenGraph() api.InitPlugin() api.InitRole() + api.InitImage() root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 67dbc0419..ac89a1f71 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -126,9 +126,10 @@ func setupTestHelper(enterprise bool) *TestHelper { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true }) - utils.SetIsLicensed(enterprise) if enterprise { - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) + } else { + th.App.SetLicense(nil) } th.Client = th.CreateClient() diff --git a/api4/file.go b/api4/file.go index 48ee281fe..acc4c78e5 100644 --- a/api4/file.go +++ b/api4/file.go @@ -281,13 +281,13 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { if len(hash) == 0 { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+utils.T(c.Err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) return } if hash != app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt) { c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest) - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+utils.T(c.Err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, c.Err, c.App.AsymmetricSigningKey()) return } diff --git a/api4/image.go b/api4/image.go new file mode 100644 index 000000000..4589de204 --- /dev/null +++ b/api4/image.go @@ -0,0 +1,22 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" +) + +func (api *API) InitImage() { + api.BaseRoutes.Image.Handle("", api.ApiSessionRequiredTrustRequester(getImage)).Methods("GET") +} + +func getImage(c *Context, w http.ResponseWriter, r *http.Request) { + // Only redirect to our image proxy if one is enabled. Arbitrary redirects are not allowed for + // security reasons. + if transform := c.App.ImageProxyAdder(); transform != nil { + http.Redirect(w, r, transform(r.URL.Query().Get("url")), http.StatusFound) + } else { + http.NotFound(w, r) + } +} diff --git a/api4/image_test.go b/api4/image_test.go new file mode 100644 index 000000000..236d5785d --- /dev/null +++ b/api4/image_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" +) + +func TestGetImage(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.Client.HttpClient.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + + originURL := "http://foo.bar/baz.gif" + + r, err := http.NewRequest("GET", th.Client.ApiUrl+"/image?url="+url.QueryEscape(originURL), nil) + require.NoError(t, err) + r.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken) + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = nil + }) + + resp, err := th.Client.HttpClient.Do(r) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = model.NewString("willnorris/imageproxy") + cfg.ServiceSettings.ImageProxyURL = model.NewString("https://proxy.foo.bar") + }) + + r, err = http.NewRequest("GET", th.Client.ApiUrl+"/image?url="+originURL, nil) + require.NoError(t, err) + r.Header.Set(model.HEADER_AUTH, th.Client.AuthType+" "+th.Client.AuthToken) + + resp, err = th.Client.HttpClient.Do(r) + require.NoError(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, "https://proxy.foo.bar//"+originURL, resp.Header.Get("Location")) +} diff --git a/api4/oauth.go b/api4/oauth.go index 655adaaee..d0f43256a 100644 --- a/api4/oauth.go +++ b/api4/oauth.go @@ -313,7 +313,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Config().ServiceSettings.EnableOAuthServiceProvider { err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented) - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) return } @@ -326,13 +326,13 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { } if err := authRequest.IsValid(); err != nil { - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) return } oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId) if err != nil { - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) return } @@ -343,7 +343,8 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { } if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) { - utils.RenderWebError(model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest), w, r) + err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) return } @@ -360,7 +361,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.Session.UserId, authRequest) if err != nil { - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) return } @@ -441,7 +442,10 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") if len(code) == 0 { - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?type=oauth_missing_code&service="+strings.Title(service), http.StatusTemporaryRedirect) + utils.RenderWebError(w, r, http.StatusTemporaryRedirect, url.Values{ + "type": []string{"oauth_missing_code"}, + "service": []string{strings.Title(service)}, + }, c.App.AsymmetricSigningKey()) return } @@ -462,7 +466,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { if action == model.OAUTH_ACTION_MOBILE { w.Write([]byte(err.ToJson())) } else { - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+url.QueryEscape(err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) } return } @@ -474,7 +478,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { if action == model.OAUTH_ACTION_MOBILE { w.Write([]byte(err.ToJson())) } else { - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+url.QueryEscape(err.Message), http.StatusTemporaryRedirect) + utils.RenderWebAppError(w, r, err, c.App.AsymmetricSigningKey()) } return } @@ -559,7 +563,9 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } if !c.App.Config().TeamSettings.EnableUserCreation { - http.Redirect(w, r, c.GetSiteURLHeader()+"/error?message="+url.QueryEscape(utils.T("api.oauth.singup_with_oauth.disabled.app_error")), http.StatusTemporaryRedirect) + utils.RenderWebError(w, r, http.StatusBadRequest, url.Values{ + "message": []string{utils.T("api.oauth.singup_with_oauth.disabled.app_error")}, + }, c.App.AsymmetricSigningKey()) return } diff --git a/api4/oauth_test.go b/api4/oauth_test.go index 0959442f0..0862f13f5 100644 --- a/api4/oauth_test.go +++ b/api4/oauth_test.go @@ -18,17 +18,16 @@ func TestCreateOAuthApp(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, IsTrusted: true} rapp, resp := AdminClient.CreateOAuthApp(oapp) @@ -90,16 +89,14 @@ func TestUpdateOAuthApp(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{ Name: "oapp", @@ -207,16 +204,14 @@ func TestGetOAuthApps(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -281,16 +276,14 @@ func TestGetOAuthApp(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -357,16 +350,14 @@ func TestGetOAuthAppInfo(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -433,16 +424,14 @@ func TestDeleteOAuthApp(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -503,16 +492,14 @@ func TestRegenerateOAuthAppSecret(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider defaultRolePermissions := th.SaveDefaultRolePermissions() defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) th.RestoreDefaultRolePermissions(defaultRolePermissions) }() - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) // Grant permission to regular users. th.AddPermissionToRole(model.PERMISSION_MANAGE_OAUTH.Id, model.SYSTEM_USER_ROLE_ID) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} @@ -637,10 +624,6 @@ func TestAuthorizeOAuthApp(t *testing.T) { Client := th.Client AdminClient := th.SystemAdminClient - enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth }) - }() th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = true }) oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} diff --git a/api4/post_test.go b/api4/post_test.go index dba3a1c2d..1b682e38b 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -17,7 +17,6 @@ import ( "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func TestCreatePost(t *testing.T) { @@ -130,14 +129,6 @@ func testCreatePostWithOutgoingHook( team := th.BasicTeam channel := th.BasicChannel - enableOutgoingHooks := th.App.Config().ServiceSettings.EnableOutgoingWebhooks - allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks }) - th.App.UpdateConfig(func(cfg *model.Config) { - cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections - }) - }() th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOutgoingWebhooks = true }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" @@ -484,15 +475,7 @@ func TestUpdatePost(t *testing.T) { Client := th.Client channel := th.BasicChannel - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) post := &model.Post{ChannelId: channel.Id, Message: "zz" + model.NewId() + "a"} rpost, resp := Client.CreatePost(post) @@ -563,15 +546,7 @@ func TestPatchPost(t *testing.T) { Client := th.Client channel := th.BasicChannel - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) post := &model.Post{ ChannelId: channel.Id, diff --git a/api4/role.go b/api4/role.go index ac9364c35..e7654011d 100644 --- a/api4/role.go +++ b/api4/role.go @@ -7,7 +7,6 @@ import ( "net/http" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func (api *API) InitRole() { @@ -86,7 +85,7 @@ func patchRole(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !utils.IsLicensed() && patch.Permissions != nil { + if c.App.License() == nil && patch.Permissions != nil { allowedPermissions := []string{ model.PERMISSION_CREATE_TEAM.Id, model.PERMISSION_MANAGE_WEBHOOKS.Id, diff --git a/api4/role_test.go b/api4/role_test.go index a3e6d35be..3fbf6808d 100644 --- a/api4/role_test.go +++ b/api4/role_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func TestGetRole(t *testing.T) { @@ -192,15 +191,7 @@ func TestPatchRole(t *testing.T) { CheckNotImplementedStatus(t, resp) // Add a license. - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) // Try again, should succeed received, resp = th.SystemAdminClient.PatchRole(role.Id, patch) diff --git a/api4/system.go b/api4/system.go index 43b941247..061ffe094 100644 --- a/api4/system.go +++ b/api4/system.go @@ -266,7 +266,7 @@ func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { return } - etag := utils.GetClientLicenseEtag(true) + etag := c.App.GetClientLicenseEtag(true) if c.HandleEtag(etag, "Get Client License", w, r) { return } @@ -274,9 +274,9 @@ func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { var clientLicense map[string]string if c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - clientLicense = utils.ClientLicense() + clientLicense = c.App.ClientLicense() } else { - clientLicense = utils.GetSanitizedClientLicense() + clientLicense = c.App.GetSanitizedClientLicense() } w.Header().Set(model.HEADER_ETAG_SERVER, etag) diff --git a/api4/team_test.go b/api4/team_test.go index 272b7372e..fa139faae 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -1250,7 +1250,7 @@ func TestAddTeamMember(t *testing.T) { tm, resp := Client.AddTeamMember(team.Id, otherUser.Id) CheckForbiddenStatus(t, resp) if resp.Error == nil { - t.Fatalf("Error is nhul") + t.Fatalf("Error is nil") } Client.Logout() @@ -1339,6 +1339,7 @@ func TestAddTeamMember(t *testing.T) { dataObject := make(map[string]string) dataObject["time"] = fmt.Sprintf("%v", model.GetMillis()) dataObject["id"] = team.Id + dataObject["invite_id"] = team.InviteId data := model.MapToJson(dataObject) hashed := utils.HashSha256(fmt.Sprintf("%v:%v", data, th.App.Config().EmailSettings.InviteSalt)) @@ -1862,10 +1863,6 @@ func TestInviteUsersToTeam(t *testing.T) { } } - restrictCreationToDomains := th.App.Config().TeamSettings.RestrictCreationToDomains - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.RestrictCreationToDomains = restrictCreationToDomains }) - }() th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.RestrictCreationToDomains = "@example.com" }) err := th.App.InviteNewUsersToTeam(emailList, th.BasicTeam.Id, th.BasicUser.Id) diff --git a/api4/user_test.go b/api4/user_test.go index a7b7d297d..4613a8ea9 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1566,18 +1566,7 @@ func TestUpdateUserMfa(t *testing.T) { defer th.TearDown() Client := th.Client - isLicensed := utils.IsLicensed() - license := utils.License() - enableMfa := *th.App.Config().ServiceSettings.EnableMultifactorAuthentication - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() - *utils.License().Features.MFA = true + th.App.SetLicense(model.NewTestLicense("mfa")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true }) session, _ := th.App.GetSession(Client.AuthToken) @@ -1612,18 +1601,7 @@ func TestCheckUserMfa(t *testing.T) { t.Fatal("should be false - mfa not active") } - isLicensed := utils.IsLicensed() - license := utils.License() - enableMfa := *th.App.Config().ServiceSettings.EnableMultifactorAuthentication - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() - *utils.License().Features.MFA = true + th.App.SetLicense(model.NewTestLicense("mfa")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true }) th.LoginBasic() @@ -1659,18 +1637,7 @@ func TestGenerateMfaSecret(t *testing.T) { _, resp = Client.GenerateMfaSecret("junk") CheckBadRequestStatus(t, resp) - isLicensed := utils.IsLicensed() - license := utils.License() - enableMfa := *th.App.Config().ServiceSettings.EnableMultifactorAuthentication - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() - *utils.License().Features.MFA = true + th.App.SetLicense(model.NewTestLicense("mfa")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true }) _, resp = Client.GenerateMfaSecret(model.NewId()) @@ -2187,19 +2154,7 @@ func TestSwitchAccount(t *testing.T) { t.Fatal("bad link") } - isLicensed := utils.IsLicensed() - license := utils.License() - enableAuthenticationTransfer := *th.App.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer = enableAuthenticationTransfer - }) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer = false }) sr = &model.SwitchRequest{ diff --git a/app/admin.go b/app/admin.go index b838ed3bd..154fa8899 100644 --- a/app/admin.go +++ b/app/admin.go @@ -237,7 +237,8 @@ func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError { return err } else { T := utils.GetUserTranslations(user.Locale) - if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg); err != nil { + license := a.License() + if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance); err != nil { return err } } diff --git a/app/app.go b/app/app.go index ffb903b57..9d44c358c 100644 --- a/app/app.go +++ b/app/app.go @@ -4,6 +4,7 @@ package app import ( + "crypto/ecdsa" "html/template" "net" "net/http" @@ -61,14 +62,19 @@ type App struct { configFile string configListeners map[string]func(*model.Config, *model.Config) + licenseValue atomic.Value + clientLicenseValue atomic.Value + licenseListeners map[string]func() + newStore func() store.Store - htmlTemplateWatcher *utils.HTMLTemplateWatcher - sessionCache *utils.Cache - configListenerId string - licenseListenerId string - disableConfigWatch bool - configWatcher *utils.ConfigWatcher + htmlTemplateWatcher *utils.HTMLTemplateWatcher + sessionCache *utils.Cache + configListenerId string + licenseListenerId string + disableConfigWatch bool + configWatcher *utils.ConfigWatcher + asymmetricSigningKey *ecdsa.PrivateKey pluginCommands []*PluginCommand pluginCommandsLock sync.RWMutex @@ -82,7 +88,7 @@ var appCount = 0 // New creates a new App. You must call Shutdown when you're done with it. // XXX: For now, only one at a time is allowed as some resources are still shared. -func New(options ...Option) (*App, error) { +func New(options ...Option) (outApp *App, outErr error) { appCount++ if appCount > 1 { panic("Only one App should exist at a time. Did you forget to call Shutdown()?") @@ -93,11 +99,17 @@ func New(options ...Option) (*App, error) { Srv: &Server{ Router: mux.NewRouter(), }, - sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), - configFile: "config.json", - configListeners: make(map[string]func(*model.Config, *model.Config)), - clientConfig: make(map[string]string), - } + sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), + configFile: "config.json", + configListeners: make(map[string]func(*model.Config, *model.Config)), + clientConfig: make(map[string]string), + licenseListeners: map[string]func(){}, + } + defer func() { + if outErr != nil { + app.Shutdown() + } + }() for _, option := range options { option(app) @@ -120,7 +132,7 @@ func New(options ...Option) (*App, error) { app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { app.configOrLicenseListener() }) - app.licenseListenerId = utils.AddLicenseListener(app.configOrLicenseListener) + app.licenseListenerId = app.AddLicenseListener(app.configOrLicenseListener) app.regenerateClientConfig() l4g.Info(utils.T("api.server.new_server.init.info")) @@ -140,6 +152,10 @@ func New(options ...Option) (*App, error) { } app.Srv.Store = app.newStore() + if err := app.ensureAsymmetricSigningKey(); err != nil { + return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key") + } + app.initJobs() app.initBuiltInPlugins() @@ -171,7 +187,9 @@ func (a *App) Shutdown() { a.ShutDownPlugins() a.WaitForGoroutines() - a.Srv.Store.Close() + if a.Srv.Store != nil { + a.Srv.Store.Close() + } a.Srv = nil if a.htmlTemplateWatcher != nil { @@ -179,7 +197,7 @@ func (a *App) Shutdown() { } a.RemoveConfigListener(a.configListenerId) - utils.RemoveLicenseListener(a.licenseListenerId) + a.RemoveLicenseListener(a.licenseListenerId) l4g.Info(utils.T("api.server.stop_server.stopped.info")) a.DisableConfigWatch() @@ -448,7 +466,7 @@ func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)) - utils.RenderWebError(err, w, r) + utils.RenderWebAppError(w, r, err, a.AsymmetricSigningKey()) } // This function migrates the default built in roles from code/config to the database. @@ -460,7 +478,7 @@ func (a *App) DoAdvancedPermissionsMigration() { l4g.Info("Migrating roles to database.") roles := model.MakeDefaultRoles() - roles = utils.SetRolePermissionsFromConfig(roles, a.Config(), utils.IsLicensed()) + roles = utils.SetRolePermissionsFromConfig(roles, a.Config(), a.License() != nil) allSucceeded := true diff --git a/app/app_test.go b/app/app_test.go index 4a4238197..f31e0332c 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -226,16 +226,12 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { } // Add a license and change the policy config. - isLicensed := utils.IsLicensed() - license := utils.License() restrictPublicChannel := *th.App.Config().TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *th.App.Config().TeamSettings.RestrictPrivateChannelManagement defer func() { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel }) - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) }() th.App.UpdateConfig(func(cfg *model.Config) { @@ -244,9 +240,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN }) - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) // Check the migration doesn't change anything if run again. th.App.DoAdvancedPermissionsMigration() @@ -394,7 +388,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { } // Remove the license. - utils.SetIsLicensed(false) + th.App.SetLicense(nil) // Do the migration again. th.ResetRoleMigration() diff --git a/app/apptestlib.go b/app/apptestlib.go index 7a2d7157d..3402f1f79 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -114,9 +114,10 @@ func setupTestHelper(enterprise bool) *TestHelper { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true }) - utils.SetIsLicensed(enterprise) if enterprise { - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) + } else { + th.App.SetLicense(nil) } return th diff --git a/app/config.go b/app/config.go index a2398f9e9..b4925e8fb 100644 --- a/app/config.go +++ b/app/config.go @@ -4,7 +4,12 @@ package app import ( + "crypto/ecdsa" + "crypto/elliptic" "crypto/md5" + "crypto/rand" + "crypto/x509" + "encoding/base64" "encoding/json" "fmt" "runtime/debug" @@ -116,8 +121,91 @@ func (a *App) InvokeConfigListeners(old, current *model.Config) { } } +// EnsureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to +// AsymmetricSigningKey will always return a valid signing key. +func (a *App) ensureAsymmetricSigningKey() error { + if a.asymmetricSigningKey != nil { + return nil + } + + var key *model.SystemAsymmetricSigningKey + + result := <-a.Srv.Store.System().GetByName(model.SYSTEM_ASYMMETRIC_SIGNING_KEY) + if result.Err == nil { + if err := json.Unmarshal([]byte(result.Data.(*model.System).Value), &key); err != nil { + return err + } + } + + // If we don't already have a key, try to generate one. + if key == nil { + newECDSAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + newKey := &model.SystemAsymmetricSigningKey{ + ECDSAKey: &model.SystemECDSAKey{ + Curve: "P-256", + X: newECDSAKey.X, + Y: newECDSAKey.Y, + D: newECDSAKey.D, + }, + } + system := &model.System{ + Name: model.SYSTEM_ASYMMETRIC_SIGNING_KEY, + } + v, err := json.Marshal(newKey) + if err != nil { + return err + } + system.Value = string(v) + if result = <-a.Srv.Store.System().Save(system); result.Err == nil { + // If we were able to save the key, use it, otherwise ignore the error. + key = newKey + } + } + + // If we weren't able to save a new key above, another server must have beat us to it. Get the + // key from the database, and if that fails, error out. + if key == nil { + result := <-a.Srv.Store.System().GetByName(model.SYSTEM_ASYMMETRIC_SIGNING_KEY) + if result.Err != nil { + return result.Err + } else if err := json.Unmarshal([]byte(result.Data.(*model.System).Value), &key); err != nil { + return err + } + } + + var curve elliptic.Curve + switch key.ECDSAKey.Curve { + case "P-256": + curve = elliptic.P256() + default: + return fmt.Errorf("unknown curve: " + key.ECDSAKey.Curve) + } + a.asymmetricSigningKey = &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: curve, + X: key.ECDSAKey.X, + Y: key.ECDSAKey.Y, + }, + D: key.ECDSAKey.D, + } + a.regenerateClientConfig() + return nil +} + +// AsymmetricSigningKey will return a private key that can be used for asymmetric signing. +func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey { + return a.asymmetricSigningKey +} + func (a *App) regenerateClientConfig() { - a.clientConfig = utils.GenerateClientConfig(a.Config(), a.DiagnosticId()) + a.clientConfig = utils.GenerateClientConfig(a.Config(), a.DiagnosticId(), a.License()) + if key := a.AsymmetricSigningKey(); key != nil { + der, _ := x509.MarshalPKIXPublicKey(&key.PublicKey) + a.clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der) + } clientConfigJSON, _ := json.Marshal(a.clientConfig) a.clientConfigHash = fmt.Sprintf("%x", md5.Sum(clientConfigJSON)) } @@ -166,11 +254,3 @@ func (a *App) Desanitize(cfg *model.Config) { cfg.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i] } } - -// License returns the currently active license or nil if the application is unlicensed. -func (a *App) License() *model.License { - if utils.IsLicensed() { - return utils.License() - } - return nil -} diff --git a/app/config_test.go b/app/config_test.go index e3d50b958..5ee999f0f 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -6,6 +6,8 @@ package app import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/model" ) @@ -54,3 +56,10 @@ func TestConfigListener(t *testing.T) { t.Fatal("listener 2 should've been called") } } + +func TestAsymmetricSigningKey(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + assert.NotNil(t, th.App.AsymmetricSigningKey()) + assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"]) +} diff --git a/app/email.go b/app/email.go index b809b972d..54a272a3b 100644 --- a/app/email.go +++ b/app/email.go @@ -276,6 +276,7 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []st props["display_name"] = team.DisplayName props["name"] = team.Name props["time"] = fmt.Sprintf("%v", model.GetMillis()) + props["invite_id"] = team.InviteId data := model.MapToJson(props) hash := utils.HashSha256(fmt.Sprintf("%v:%v", data, a.Config().EmailSettings.InviteSalt)) bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash)) @@ -316,5 +317,6 @@ func (a *App) NewEmailTemplate(name, locale string) *utils.HTMLTemplate { } func (a *App) SendMail(to, subject, htmlBody string) *model.AppError { - return utils.SendMailUsingConfig(to, subject, htmlBody, a.Config()) + license := a.License() + return utils.SendMailUsingConfig(to, subject, htmlBody, a.Config(), license != nil && *license.Features.Compliance) } diff --git a/app/file.go b/app/file.go index d66c64adb..bb20585bb 100644 --- a/app/file.go +++ b/app/file.go @@ -58,7 +58,8 @@ const ( ) func (a *App) FileBackend() (utils.FileBackend, *model.AppError) { - return utils.NewFileBackend(&a.Config().FileSettings) + license := a.License() + return utils.NewFileBackend(&a.Config().FileSettings, license != nil && *license.Features.Compliance) } func (a *App) ReadFile(path string) ([]byte, *model.AppError) { diff --git a/app/license.go b/app/license.go index c7fd07197..6a2206747 100644 --- a/app/license.go +++ b/app/license.go @@ -4,16 +4,19 @@ package app import ( + "crypto/md5" + "fmt" "net/http" "strings" l4g "github.com/alecthomas/log4go" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) func (a *App) LoadLicense() { - utils.RemoveLicense() + a.SetLicense(nil) licenseId := "" if result := <-a.Srv.Store.System().Get(); result.Err == nil { @@ -36,7 +39,7 @@ func (a *App) LoadLicense() { if result := <-a.Srv.Store.License().Get(licenseId); result.Err == nil { record := result.Data.(*model.LicenseRecord) - utils.LoadLicense([]byte(record.Bytes)) + a.ValidateAndSetLicenseBytes([]byte(record.Bytes)) l4g.Info("License key valid unlocking enterprise features.") } else { l4g.Info(utils.T("mattermost.load_license.find.warn")) @@ -59,7 +62,7 @@ func (a *App) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) } } - if ok := utils.SetLicense(license); !ok { + if ok := a.SetLicense(license); !ok { return nil, model.NewAppError("addLicense", model.EXPIRED_LICENSE_ERROR, nil, "", http.StatusBadRequest) } @@ -102,21 +105,114 @@ func (a *App) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) return license, nil } +// License returns the currently active license or nil if the application is unlicensed. +func (a *App) License() *model.License { + license, _ := a.licenseValue.Load().(*model.License) + return license +} + +func (a *App) SetLicense(license *model.License) bool { + defer func() { + for _, listener := range a.licenseListeners { + listener() + } + }() + + if license != nil { + license.Features.SetDefaults() + + if !license.IsExpired() { + a.licenseValue.Store(license) + a.clientLicenseValue.Store(utils.GetClientLicense(license)) + return true + } + } + + a.licenseValue.Store((*model.License)(nil)) + a.SetClientLicense(map[string]string{"IsLicensed": "false"}) + return false +} + +func (a *App) ValidateAndSetLicenseBytes(b []byte) { + if success, licenseStr := utils.ValidateLicense(b); success { + license := model.LicenseFromJson(strings.NewReader(licenseStr)) + a.SetLicense(license) + return + } + + l4g.Warn(utils.T("utils.license.load_license.invalid.warn")) +} + +func (a *App) SetClientLicense(m map[string]string) { + a.clientLicenseValue.Store(m) +} + +func (a *App) ClientLicense() map[string]string { + clientLicense, _ := a.clientLicenseValue.Load().(map[string]string) + return clientLicense +} + func (a *App) RemoveLicense() *model.AppError { - utils.RemoveLicense() + if license, _ := a.licenseValue.Load().(*model.License); license == nil { + return nil + } sysVar := &model.System{} sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID sysVar.Value = "" if result := <-a.Srv.Store.System().SaveOrUpdate(sysVar); result.Err != nil { - utils.RemoveLicense() return result.Err } + a.SetLicense(nil) a.ReloadConfig() a.InvalidateAllCaches() return nil } + +func (a *App) AddLicenseListener(listener func()) string { + id := model.NewId() + a.licenseListeners[id] = listener + return id +} + +func (a *App) RemoveLicenseListener(id string) { + delete(a.licenseListeners, id) +} + +func (a *App) GetClientLicenseEtag(useSanitized bool) string { + value := "" + + lic := a.ClientLicense() + + if useSanitized { + lic = a.GetSanitizedClientLicense() + } + + for k, v := range lic { + value += fmt.Sprintf("%s:%s;", k, v) + } + + return model.Etag(fmt.Sprintf("%x", md5.Sum([]byte(value)))) +} + +func (a *App) GetSanitizedClientLicense() map[string]string { + sanitizedLicense := make(map[string]string) + + for k, v := range a.ClientLicense() { + sanitizedLicense[k] = v + } + + delete(sanitizedLicense, "Id") + delete(sanitizedLicense, "Name") + delete(sanitizedLicense, "Email") + delete(sanitizedLicense, "PhoneNumber") + delete(sanitizedLicense, "IssuedAt") + delete(sanitizedLicense, "StartsAt") + delete(sanitizedLicense, "ExpiresAt") + + return sanitizedLicense +} diff --git a/app/license_test.go b/app/license_test.go index 5b73d9d18..f86d604d1 100644 --- a/app/license_test.go +++ b/app/license_test.go @@ -4,8 +4,9 @@ package app import ( - //"github.com/mattermost/mattermost-server/model" "testing" + + "github.com/mattermost/mattermost-server/model" ) func TestLoadLicense(t *testing.T) { @@ -37,3 +38,75 @@ func TestRemoveLicense(t *testing.T) { t.Fatal("should have removed license") } } + +func TestSetLicense(t *testing.T) { + th := Setup() + defer th.TearDown() + + l1 := &model.License{} + l1.Features = &model.Features{} + l1.Customer = &model.Customer{} + l1.StartsAt = model.GetMillis() - 1000 + l1.ExpiresAt = model.GetMillis() + 100000 + if ok := th.App.SetLicense(l1); !ok { + t.Fatal("license should have worked") + } + + l2 := &model.License{} + l2.Features = &model.Features{} + l2.Customer = &model.Customer{} + l2.StartsAt = model.GetMillis() - 1000 + l2.ExpiresAt = model.GetMillis() - 100 + if ok := th.App.SetLicense(l2); ok { + t.Fatal("license should have failed") + } + + l3 := &model.License{} + l3.Features = &model.Features{} + l3.Customer = &model.Customer{} + l3.StartsAt = model.GetMillis() + 10000 + l3.ExpiresAt = model.GetMillis() + 100000 + if ok := th.App.SetLicense(l3); !ok { + t.Fatal("license should have passed") + } +} + +func TestClientLicenseEtag(t *testing.T) { + th := Setup() + defer th.TearDown() + + etag1 := th.App.GetClientLicenseEtag(false) + + th.App.SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) + + etag2 := th.App.GetClientLicenseEtag(false) + if etag1 == etag2 { + t.Fatal("etags should not match") + } + + th.App.SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "false"}) + + etag3 := th.App.GetClientLicenseEtag(false) + if etag2 == etag3 { + t.Fatal("etags should not match") + } +} + +func TestGetSanitizedClientLicense(t *testing.T) { + th := Setup() + defer th.TearDown() + + l1 := &model.License{} + l1.Features = &model.Features{} + l1.Customer = &model.Customer{} + l1.Customer.Name = "TestName" + l1.StartsAt = model.GetMillis() - 1000 + l1.ExpiresAt = model.GetMillis() + 100000 + th.App.SetLicense(l1) + + m := th.App.GetSanitizedClientLicense() + + if _, ok := m["Name"]; ok { + t.Fatal("should have been sanatized") + } +} diff --git a/app/post.go b/app/post.go index 843319082..3f6252faa 100644 --- a/app/post.go +++ b/app/post.go @@ -869,6 +869,10 @@ func (a *App) imageProxyConfig() (proxyType, proxyURL, options, siteURL string) proxyURL += "/" } + if siteURL == "" || siteURL[len(siteURL)-1] != '/' { + siteURL += "/" + } + if cfg.ServiceSettings.ImageProxyOptions != nil { options = *cfg.ServiceSettings.ImageProxyOptions } @@ -883,14 +887,10 @@ func (a *App) ImageProxyAdder() func(string) string { } return func(url string) string { - if url == "" || strings.HasPrefix(url, proxyURL) { + if url == "" || url[0] == '/' || strings.HasPrefix(url, siteURL) || strings.HasPrefix(url, proxyURL) { return url } - if url[0] == '/' { - url = siteURL + url - } - switch proxyType { case "atmos/camo": mac := hmac.New(sha1.New, []byte(options)) diff --git a/app/post_test.go b/app/post_test.go index 049d3ff92..207935d58 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func TestUpdatePostEditAt(t *testing.T) { @@ -51,15 +50,7 @@ func TestUpdatePostTimeLimit(t *testing.T) { post := &model.Post{} *post = *th.BasicPost - isLicensed := utils.IsLicensed() - license := utils.License() - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() + th.App.SetLicense(model.NewTestLicense()) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = -1 @@ -236,6 +227,10 @@ func TestImageProxy(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.SiteURL = "http://mymattermost.com" + }) + for name, tc := range map[string]struct { ProxyType string ProxyURL string @@ -257,6 +252,18 @@ func TestImageProxy(t *testing.T) { ImageURL: "http://mydomain.com/myimage", ProxiedImageURL: "https://127.0.0.1/x1000/http://mydomain.com/myimage", }, + "willnorris/imageproxy_SameSite": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ImageURL: "http://mymattermost.com/myimage", + ProxiedImageURL: "http://mymattermost.com/myimage", + }, + "willnorris/imageproxy_PathOnly": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ImageURL: "/myimage", + ProxiedImageURL: "/myimage", + }, "willnorris/imageproxy_EmptyImageURL": { ProxyType: "willnorris/imageproxy", ProxyURL: "https://127.0.0.1", diff --git a/app/session_test.go b/app/session_test.go index bca3b59b7..bf8198a4e 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -6,11 +6,10 @@ package app import ( "testing" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" ) func TestCache(t *testing.T) { @@ -48,18 +47,7 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { session, _ = th.App.CreateSession(session) - isLicensed := utils.IsLicensed() - license := utils.License() - timeout := *th.App.Config().ServiceSettings.SessionIdleTimeoutInMinutes - defer func() { - utils.SetIsLicensed(isLicensed) - utils.SetLicense(license) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionIdleTimeoutInMinutes = timeout }) - }() - utils.SetIsLicensed(true) - utils.SetLicense(&model.License{Features: &model.Features{}}) - utils.License().Features.SetDefaults() - *utils.License().Features.Compliance = true + th.App.SetLicense(model.NewTestLicense("compliance")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionIdleTimeoutInMinutes = 5 }) rsession, err := th.App.GetSession(session.Token) @@ -122,7 +110,7 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { assert.Nil(t, err) // Test regular session with license off, should not timeout - *utils.License().Features.Compliance = false + th.App.SetLicense(nil) session = &model.Session{ UserId: model.NewId(), @@ -136,7 +124,7 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { _, err = th.App.GetSession(session.Token) assert.Nil(t, err) - *utils.License().Features.Compliance = true + th.App.SetLicense(model.NewTestLicense("compliance")) // Test regular session with timeout set to 0, should not timeout th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionIdleTimeoutInMinutes = 0 }) diff --git a/app/team.go b/app/team.go index 5a81bd220..dc10760f8 100644 --- a/app/team.go +++ b/app/team.go @@ -238,6 +238,11 @@ func (a *App) AddUserToTeamByHash(userId string, hash string, data string) (*mod team = result.Data.(*model.Team) } + // verify that the team's invite id hasn't been changed since the invite was sent + if team.InviteId != props["invite_id"] { + return nil, model.NewAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_mismatched_invite_id.app_error", nil, "", http.StatusBadRequest) + } + var user *model.User if result := <-uchan; result.Err != nil { return nil, result.Err diff --git a/app/team_test.go b/app/team_test.go index 084558fb4..7cb20b6f6 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -7,7 +7,15 @@ import ( "strings" "testing" + "fmt" + + "sync/atomic" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "github.com/mattermost/mattermost-server/store/storetest" + "github.com/mattermost/mattermost-server/utils" + "github.com/stretchr/testify/assert" ) func TestCreateTeam(t *testing.T) { @@ -393,3 +401,62 @@ func TestSanitizeTeams(t *testing.T) { } }) } + +func TestAddUserToTeamByHashMismatchedInviteId(t *testing.T) { + mockStore := &storetest.Store{} + defer mockStore.AssertExpectations(t) + + teamId := model.NewId() + userId := model.NewId() + inviteSalt := model.NewId() + + inviteId := model.NewId() + teamInviteId := model.NewId() + + // generate a fake email invite - stolen from SendInviteEmails() in email.go + props := make(map[string]string) + props["email"] = model.NewId() + "@mattermost.com" + props["id"] = teamId + props["display_name"] = model.NewId() + props["name"] = model.NewId() + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + props["invite_id"] = inviteId + data := model.MapToJson(props) + hash := utils.HashSha256(fmt.Sprintf("%v:%v", data, inviteSalt)) + + // when the server tries to validate the invite, it will pull the user from our mock store + // this can return nil, because we'll fail before we get to trying to use it + mockStore.UserStore.On("Get", userId).Return( + storetest.NewStoreChannel(store.StoreResult{ + Data: nil, + Err: nil, + }), + ) + + // the server will also pull the team. the one we return has a different invite id than the one in the email invite we made above + mockStore.TeamStore.On("Get", teamId).Return( + storetest.NewStoreChannel(store.StoreResult{ + Data: &model.Team{ + InviteId: teamInviteId, + }, + Err: nil, + }), + ) + + app := App{ + Srv: &Server{ + Store: mockStore, + }, + config: atomic.Value{}, + } + app.config.Store(&model.Config{ + EmailSettings: model.EmailSettings{ + InviteSalt: inviteSalt, + }, + }) + + // this should fail because the invite ids are mismatched + team, err := app.AddUserToTeamByHash(userId, hash, data) + assert.Nil(t, team) + assert.Equal(t, "api.user.create_user.signup_link_mismatched_invite_id.app_error", err.Id) +} diff --git a/cmd/platform/jobserver.go b/cmd/platform/jobserver.go index e664136c0..044ee6b6a 100644 --- a/cmd/platform/jobserver.go +++ b/cmd/platform/jobserver.go @@ -35,7 +35,7 @@ func jobserverCmdF(cmd *cobra.Command, args []string) { defer l4g.Close() defer a.Shutdown() - a.Jobs.LoadLicense() + a.LoadLicense() // Run jobs l4g.Info("Starting Mattermost job server") diff --git a/cmd/platform/server.go b/cmd/platform/server.go index 54a7d6ae3..c3001a77d 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -42,10 +42,11 @@ func runServerCmd(cmd *cobra.Command, args []string) error { disableConfigWatch, _ := cmd.Flags().GetBool("disableconfigwatch") - return runServer(config, disableConfigWatch) + interruptChan := make(chan os.Signal, 1) + return runServer(config, disableConfigWatch, interruptChan) } -func runServer(configFileLocation string, disableConfigWatch bool) error { +func runServer(configFileLocation string, disableConfigWatch bool, interruptChan chan os.Signal) error { options := []app.Option{app.ConfigFile(configFileLocation)} if disableConfigWatch { options = append(options, app.DisableConfigWatch) @@ -167,9 +168,8 @@ func runServer(configFileLocation string, disableConfigWatch bool) error { // wait for kill signal before attempting to gracefully shutdown // the running service - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - <-c + signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-interruptChan if a.Cluster != nil { a.Cluster.StopInterNodeCommunication() diff --git a/cmd/platform/server_test.go b/cmd/platform/server_test.go new file mode 100644 index 000000000..15f9a357a --- /dev/null +++ b/cmd/platform/server_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "io/ioutil" + "os" + "syscall" + "testing" + + "github.com/mattermost/mattermost-server/jobs" + "github.com/mattermost/mattermost-server/utils" + "github.com/stretchr/testify/require" +) + +type ServerTestHelper struct { + configPath string + disableConfigWatch bool + interruptChan chan os.Signal + originalInterval int +} + +func SetupServerTest() *ServerTestHelper { + // Build a channel that will be used by the server to receive system signals… + interruptChan := make(chan os.Signal, 1) + // …and sent it immediately a SIGINT value. + // This will make the server loop stop as soon as it started successfully. + interruptChan <- syscall.SIGINT + + // Let jobs poll for termination every 0.2s (instead of every 15s by default) + // Otherwise we would have to wait the whole polling duration before the test + // terminates. + originalInterval := jobs.DEFAULT_WATCHER_POLLING_INTERVAL + jobs.DEFAULT_WATCHER_POLLING_INTERVAL = 200 + + th := &ServerTestHelper{ + configPath: utils.FindConfigFile("config.json"), + disableConfigWatch: true, + interruptChan: interruptChan, + originalInterval: originalInterval, + } + return th +} + +func (th *ServerTestHelper) TearDownServerTest() { + jobs.DEFAULT_WATCHER_POLLING_INTERVAL = th.originalInterval +} + +func TestRunServerSuccess(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + err := runServer(th.configPath, th.disableConfigWatch, th.interruptChan) + require.NoError(t, err) +} + +func TestRunServerInvalidConfigFile(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + // Start the server with an unreadable config file + unreadableConfigFile, err := ioutil.TempFile("", "mattermost-unreadable-config-file-") + if err != nil { + panic(err) + } + os.Chmod(unreadableConfigFile.Name(), 0200) + defer os.Remove(unreadableConfigFile.Name()) + + err = runServer(unreadableConfigFile.Name(), th.disableConfigWatch, th.interruptChan) + require.Error(t, err) +} diff --git a/i18n/de.json b/i18n/de.json index 0f326e221..b506e7018 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -153,7 +153,7 @@ }, { "id": "api.channel.add_member.added", - "translation": "%v wurde von %v zum Kanal hinzugefügt" + "translation": "%v wurde von %v zum Kanal hinzugefügt." }, { "id": "api.channel.add_member.find_channel.app_error", @@ -337,7 +337,7 @@ }, { "id": "api.channel.post_update_channel_header_message_and_forget.post.error", - "translation": "Fehler beim Senden der Aktualisierung der Kanalüberschrift-Mitteilung" + "translation": "Fehler bei der Aktualisierung der Kanalüberschrift" }, { "id": "api.channel.post_update_channel_header_message_and_forget.removed", @@ -4644,7 +4644,7 @@ }, { "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", - "translation": "Invalid atmos/camo image proxy options for service settings. Must be set to your shared key." + "translation": "Ungültiger atmos-/camo-Bild-Proxy-Typ für Diensteinstellungen. Muss auf ihren Shared Key gesetzt sein." }, { "id": "model.config.is_valid.cluster_email_batching.app_error", @@ -5404,7 +5404,7 @@ }, { "id": "model.user.is_valid.position.app_error", - "translation": "Ungültige Position: Darf nicht länger als 35 Zeichen sein." + "translation": "Ungültige Position: Darf nicht länger als 128 Zeichen sein." }, { "id": "model.user.is_valid.pwd.app_error", @@ -5768,7 +5768,7 @@ }, { "id": "store.sql_channel.get_unread.app_error", - "translation": "Die ungelesenen Mitteilungen des Kanals konnten nicht abgerufen werden" + "translation": "Die ungelesenen Nachrichten des Kanals konnten nicht abgerufen werden" }, { "id": "store.sql_channel.increment_mention_count.app_error", @@ -6592,7 +6592,7 @@ }, { "id": "store.sql_team.get_unread.app_error", - "translation": "Die ungelesenen Mitteilungen des Teams konnten nicht abgerufen werden" + "translation": "Die ungelesenen Nachrichten des Teams konnten nicht abgerufen werden" }, { "id": "store.sql_team.permanent_delete.app_error", diff --git a/i18n/en.json b/i18n/en.json index 76f3d7ea6..5befd9fc5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2203,10 +2203,6 @@ "translation": "The signup link has expired" }, { - "id": "api.team.create_team_from_signup.invalid_link.app_error", - "translation": "The signup link does not appear to be valid" - }, - { "id": "api.team.create_team_from_signup.unavailable.app_error", "translation": "This URL is unavailable. Please try another." }, @@ -2715,6 +2711,10 @@ "translation": "The signup link has expired" }, { + "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error", + "translation": "The signup link does not appear to be valid" + }, + { "id": "api.user.create_user.signup_link_invalid.app_error", "translation": "The signup link does not appear to be valid" }, @@ -7331,10 +7331,6 @@ "translation": "Signup" }, { - "id": "web.signup_team_complete.invalid_link.app_error", - "translation": "The signup link does not appear to be valid" - }, - { "id": "web.signup_team_complete.link_expired.app_error", "translation": "The signup link has expired" }, @@ -7351,10 +7347,6 @@ "translation": "The signup link has expired" }, { - "id": "web.signup_user_complete.link_invalid.app_error", - "translation": "The signup link does not appear to be valid" - }, - { "id": "web.signup_user_complete.no_invites.app_error", "translation": "The team type doesn't allow open invites" }, diff --git a/i18n/ja.json b/i18n/ja.json index 19a88fda2..3017f94dc 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -5404,7 +5404,7 @@ }, { "id": "model.user.is_valid.position.app_error", - "translation": "不正な役職: 128文字以上にはできません" + "translation": "不正な役職: 128文字以下でなければなりません。" }, { "id": "model.user.is_valid.pwd.app_error", diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 66ff2c49b..b07cb452e 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -153,7 +153,7 @@ }, { "id": "api.channel.add_member.added", - "translation": "%v adicionado ao canal por %v" + "translation": "%v adicionado ao canal por %v." }, { "id": "api.channel.add_member.find_channel.app_error", @@ -2184,7 +2184,7 @@ }, { "id": "api.team.add_user_to_team.added", - "translation": "%v adicionado a equipe por %v" + "translation": "%v adicionado a equipe por %v." }, { "id": "api.team.add_user_to_team.missing_parameter.app_error", @@ -4644,7 +4644,7 @@ }, { "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", - "translation": "Invalid atmos/camo image proxy options for service settings. Must be set to your shared key." + "translation": "Opções inválidas de proxy de imagem atmos/camo nas configurações de serviço. Deve ser configurado para sua chave compartilhada." }, { "id": "model.config.is_valid.cluster_email_batching.app_error", @@ -4760,7 +4760,7 @@ }, { "id": "model.config.is_valid.image_proxy_type.app_error", - "translation": "Invalid image proxy type for service settings." + "translation": "Tipo de proxy de imagem inválido nas configurações de serviços." }, { "id": "model.config.is_valid.ldap_basedn", @@ -5404,7 +5404,7 @@ }, { "id": "model.user.is_valid.position.app_error", - "translation": "Posição inválida: não pode ter mais que 35 caracteres." + "translation": "Posição inválida: não pode ter mais que 128 caracteres." }, { "id": "model.user.is_valid.pwd.app_error", diff --git a/i18n/ru.json b/i18n/ru.json index efa6c4111..b1e37d191 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -2320,7 +2320,7 @@ }, { "id": "api.team.remove_user_from_team.removed", - "translation": "%v удален из команды." + "translation": " %v удален из команды." }, { "id": "api.team.signup_team.email_disabled.app_error", diff --git a/i18n/tr.json b/i18n/tr.json index 45b047512..b95c9b542 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -153,7 +153,7 @@ }, { "id": "api.channel.add_member.added", - "translation": "%v kanala %v tarafından eklendi" + "translation": "%v kanala %v tarafından eklendi." }, { "id": "api.channel.add_member.find_channel.app_error", @@ -2184,7 +2184,7 @@ }, { "id": "api.team.add_user_to_team.added", - "translation": "%v takıma %v tarafından eklendi" + "translation": "%v takıma %v tarafından eklendi." }, { "id": "api.team.add_user_to_team.missing_parameter.app_error", @@ -5404,7 +5404,7 @@ }, { "id": "model.user.is_valid.position.app_error", - "translation": "Konum geçersiz: 35 karakterden uzun olmamalıdır." + "translation": "Konum geçersiz: 128 karakterden kısa olmalıdır." }, { "id": "model.user.is_valid.pwd.app_error", diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index cbd7db862..a7387d88f 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -153,7 +153,7 @@ }, { "id": "api.channel.add_member.added", - "translation": "%v 由 %v 邀請加入頻道" + "translation": "%v 已被 %v 加入頻道。" }, { "id": "api.channel.add_member.find_channel.app_error", @@ -201,11 +201,11 @@ }, { "id": "api.channel.change_channel_privacy.private_to_public", - "translation": "This channel has been converted to a Public Channel and can be joined by any team member." + "translation": "此頻道已轉為公開頻道,任意團隊成員將可加入。" }, { "id": "api.channel.change_channel_privacy.public_to_private", - "translation": "This channel has been converted to a Private Channel." + "translation": "此頻道已轉為私人頻道。" }, { "id": "api.channel.create_channel.direct_channel.app_error", @@ -2184,7 +2184,7 @@ }, { "id": "api.team.add_user_to_team.added", - "translation": "%v 由 %v 邀請加入頻道" + "translation": "%v 已被 %v 加入頻道。" }, { "id": "api.team.add_user_to_team.missing_parameter.app_error", @@ -2320,7 +2320,7 @@ }, { "id": "api.team.remove_user_from_team.removed", - "translation": "%v 已從頻道中移除。" + "translation": "%v 已從團隊中移除。" }, { "id": "api.team.signup_team.email_disabled.app_error", @@ -3360,7 +3360,7 @@ }, { "id": "app.import.validate_post_import_data.create_at_zero.error", - "translation": "如果有提供訊息建立時間,該值不能為 0。" + "translation": "訊息建立時間不能為 0。" }, { "id": "app.import.validate_post_import_data.message_length.error", @@ -3380,51 +3380,51 @@ }, { "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", - "translation": "Reaction CreateAt property must be greater than the parent post CreateAt." + "translation": "互動建立時間必須大於隸屬訊息的建立時間。" }, { "id": "app.import.validate_reaction_import_data.create_at_missing.error", - "translation": "缺少必要的訊息屬性:建立日期。" + "translation": "缺少必要的互動屬性:建立日期。" }, { "id": "app.import.validate_reaction_import_data.create_at_zero.error", - "translation": "如果有提供訊息建立時間,該值不能為 0。" + "translation": "互動建立時間不能為 0。" }, { "id": "app.import.validate_reaction_import_data.emoji_name_length.error", - "translation": "訊息屬性長度超過允許的最大長度" + "translation": "互動屬性 繪文字名稱 長度超過允許的最大長度" }, { "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", - "translation": "缺少必要的訊息屬性:使用者。" + "translation": "缺少必要的互動屬性:繪文字名稱。" }, { "id": "app.import.validate_reaction_import_data.user_missing.error", - "translation": "缺少必要的訊息屬性:使用者。" + "translation": "缺少必要的互動屬性:使用者。" }, { "id": "app.import.validate_reply_import_data.create_at_before_parent.error", - "translation": "Reply CreateAt property must be greater than the parent post CreateAt." + "translation": "回應建立時間必須大於隸屬訊息的建立時間。" }, { "id": "app.import.validate_reply_import_data.create_at_missing.error", - "translation": "缺少必要的訊息屬性:建立日期。" + "translation": "缺少必要的回應屬性:建立日期。" }, { "id": "app.import.validate_reply_import_data.create_at_zero.error", - "translation": "如果有提供訊息建立時間,該值不能為 0。" + "translation": "回應建立時間不能為 0。" }, { "id": "app.import.validate_reply_import_data.message_length.error", - "translation": "訊息屬性長度超過允許的最大長度" + "translation": "回應訊息屬性長度超過允許的最大長度" }, { "id": "app.import.validate_reply_import_data.message_missing.error", - "translation": "缺少必要的訊息屬性:訊息。" + "translation": "缺少必要的回應屬性:訊息。" }, { "id": "app.import.validate_reply_import_data.user_missing.error", - "translation": "缺少必要的訊息屬性:使用者。" + "translation": "缺少必要的回應屬性:使用者。" }, { "id": "app.import.validate_team_import_data.allowed_domains_length.error", diff --git a/jobs/jobs_watcher.go b/jobs/jobs_watcher.go index f519e7cca..eaa3a4e73 100644 --- a/jobs/jobs_watcher.go +++ b/jobs/jobs_watcher.go @@ -11,9 +11,9 @@ import ( "github.com/mattermost/mattermost-server/model" ) -const ( - DEFAULT_WATCHER_POLLING_INTERVAL = 15000 -) +// Default polling interval for jobs termination. +// (Defining as `var` rather than `const` allows tests to lower the interval.) +var DEFAULT_WATCHER_POLLING_INTERVAL = 15000 type Watcher struct { srv *JobServer diff --git a/jobs/server.go b/jobs/server.go index 4015d581e..01cf821dc 100644 --- a/jobs/server.go +++ b/jobs/server.go @@ -4,12 +4,9 @@ package jobs import ( - l4g "github.com/alecthomas/log4go" - ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" - "github.com/mattermost/mattermost-server/utils" ) type ConfigService interface { @@ -50,36 +47,6 @@ func (srv *JobServer) Config() *model.Config { return srv.ConfigService.Config() } -func (srv *JobServer) LoadLicense() { - licenseId := "" - if result := <-srv.Store.System().Get(); result.Err == nil { - props := result.Data.(model.StringMap) - licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID] - } - - var licenseBytes []byte - - if len(licenseId) != 26 { - // Lets attempt to load the file from disk since it was missing from the DB - _, licenseBytes = utils.GetAndValidateLicenseFileFromDisk(*srv.ConfigService.Config().ServiceSettings.LicenseFileLocation) - } else { - if result := <-srv.Store.License().Get(licenseId); result.Err == nil { - record := result.Data.(*model.LicenseRecord) - licenseBytes = []byte(record.Bytes) - l4g.Info("License key valid unlocking enterprise features.") - } else { - l4g.Info(utils.T("mattermost.load_license.find.warn")) - } - } - - if licenseBytes != nil { - utils.LoadLicense(licenseBytes) - l4g.Info("License key valid unlocking enterprise features.") - } else { - l4g.Info(utils.T("mattermost.load_license.find.warn")) - } -} - func (srv *JobServer) StartWorkers() { srv.Workers = srv.InitWorkers().Start() } diff --git a/jobs/server_test.go b/jobs/server_test.go deleted file mode 100644 index 3b5ef6f3d..000000000 --- a/jobs/server_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package jobs - -import ( - "testing" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store" - "github.com/mattermost/mattermost-server/store/storetest" - "github.com/mattermost/mattermost-server/utils" -) - -func TestJobServer_LoadLicense(t *testing.T) { - if utils.T == nil { - utils.TranslationsPreInit() - } - - mockStore := &storetest.Store{} - defer mockStore.AssertExpectations(t) - - server := &JobServer{ - Store: mockStore, - } - - mockStore.SystemStore.On("Get").Return(storetest.NewStoreChannel(store.StoreResult{ - Data: model.StringMap{ - model.SYSTEM_ACTIVE_LICENSE_ID: "thelicenseid00000000000000", - }, - })) - mockStore.LicenseStore.On("Get", "thelicenseid00000000000000").Return(storetest.NewStoreChannel(store.StoreResult{ - Data: &model.LicenseRecord{ - Id: "thelicenseid00000000000000", - }, - })) - - server.LoadLicense() -} diff --git a/model/authorize.go b/model/authorize.go index 2296e7e22..9fd5afa70 100644 --- a/model/authorize.go +++ b/model/authorize.go @@ -62,7 +62,7 @@ func (ad *AuthData) IsValid() *AppError { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) } - if len(ad.State) > 128 { + if len(ad.State) > 1024 { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) } diff --git a/model/authorize_test.go b/model/authorize_test.go index 3f43a4fc3..81e059305 100644 --- a/model/authorize_test.go +++ b/model/authorize_test.go @@ -115,7 +115,7 @@ func TestAuthIsValid(t *testing.T) { t.Fatal(err) } - ad.Scope = NewRandomString(129) + ad.Scope = NewRandomString(1025) if err := ad.IsValid(); err == nil { t.Fatal("Should have failed invalid Scope") } diff --git a/model/client4.go b/model/client4.go index 55179bdf9..4b50aa05f 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1733,7 +1733,7 @@ func (c *Client4) RemoveUserFromChannel(channelId, userId string) (bool, *Respon // CreatePost creates a post based on the provided post struct. func (c *Client4) CreatePost(post *Post) (*Post, *Response) { - if r, err := c.DoApiPost(c.GetPostsRoute(), post.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetPostsRoute(), post.ToUnsanitizedJson()); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) @@ -1743,7 +1743,7 @@ func (c *Client4) CreatePost(post *Post) (*Post, *Response) { // UpdatePost updates a post based on the provided post struct. func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response) { - if r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToJson()); err != nil { + if r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToUnsanitizedJson()); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) diff --git a/model/client4_test.go b/model/client4_test.go new file mode 100644 index 000000000..f7923fa8f --- /dev/null +++ b/model/client4_test.go @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// https://github.com/mattermost/mattermost-server/issues/8205 +func TestClient4CreatePost(t *testing.T) { + post := &Post{ + Props: map[string]interface{}{ + "attachments": []*SlackAttachment{ + &SlackAttachment{ + Actions: []*PostAction{ + &PostAction{ + Integration: &PostActionIntegration{ + Context: map[string]interface{}{ + "foo": "bar", + }, + URL: "http://foo.com", + }, + Name: "Foo", + }, + }, + }, + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attachments := PostFromJson(r.Body).Attachments() + assert.Equal(t, []*SlackAttachment{ + &SlackAttachment{ + Actions: []*PostAction{ + &PostAction{ + Integration: &PostActionIntegration{ + Context: map[string]interface{}{ + "foo": "bar", + }, + URL: "http://foo.com", + }, + Name: "Foo", + }, + }, + }, + }, attachments) + })) + + client := NewAPIv4Client(server.URL) + _, resp := client.CreatePost(post) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/model/license.go b/model/license.go index f96cba06c..942a18d55 100644 --- a/model/license.go +++ b/model/license.go @@ -173,6 +173,25 @@ func (l *License) ToJson() string { return string(b) } +// NewTestLicense returns a license that expires in the future and has the given features. +func NewTestLicense(features ...string) *License { + ret := &License{ + ExpiresAt: GetMillis() + 90*24*60*60*1000, + Customer: &Customer{}, + Features: &Features{}, + } + ret.Features.SetDefaults() + + featureMap := map[string]bool{} + for _, feature := range features { + featureMap[feature] = true + } + featureJson, _ := json.Marshal(featureMap) + json.Unmarshal(featureJson, &ret.Features) + + return ret +} + func LicenseFromJson(data io.Reader) *License { var o *License json.NewDecoder(data).Decode(&o) diff --git a/model/post.go b/model/post.go index 7cf0f1b35..4a774b5d4 100644 --- a/model/post.go +++ b/model/post.go @@ -122,12 +122,13 @@ type PostActionIntegrationResponse struct { func (o *Post) ToJson() string { copy := *o copy.StripActionIntegrations() - b, err := json.Marshal(©) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(©) + return string(b) +} + +func (o *Post) ToUnsanitizedJson() string { + b, _ := json.Marshal(o) + return string(b) } func PostFromJson(data io.Reader) *Post { diff --git a/model/system.go b/model/system.go index 020c50858..2a636b14f 100644 --- a/model/system.go +++ b/model/system.go @@ -6,14 +6,16 @@ package model import ( "encoding/json" "io" + "math/big" ) const ( - SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" - SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" - SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" - SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" - SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" + SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" + SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" + SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey" ) type System struct { @@ -31,3 +33,14 @@ func SystemFromJson(data io.Reader) *System { json.NewDecoder(data).Decode(&o) return o } + +type SystemAsymmetricSigningKey struct { + ECDSAKey *SystemECDSAKey `json:"ecdsa_key,omitempty"` +} + +type SystemECDSAKey struct { + Curve string `json:"curve"` + X *big.Int `json:"x"` + Y *big.Int `json:"y"` + D *big.Int `json:"d,omitempty"` +} diff --git a/store/sqlstore/oauth_store.go b/store/sqlstore/oauth_store.go index 30a44b75f..0a9bd8266 100644 --- a/store/sqlstore/oauth_store.go +++ b/store/sqlstore/oauth_store.go @@ -35,7 +35,7 @@ func NewSqlOAuthStore(sqlStore SqlStore) store.OAuthStore { tableAuth.ColMap("ClientId").SetMaxSize(26) tableAuth.ColMap("Code").SetMaxSize(128) tableAuth.ColMap("RedirectUri").SetMaxSize(256) - tableAuth.ColMap("State").SetMaxSize(128) + tableAuth.ColMap("State").SetMaxSize(1024) tableAuth.ColMap("Scope").SetMaxSize(128) tableAccess := db.AddTableWithName(model.AccessData{}, "OAuthAccessData").SetKeys(false, "Token") diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index bc336e70d..25c3c4913 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -322,7 +322,10 @@ type etagPosts struct { func (s SqlPostStore) InvalidateLastPostTimeCache(channelId string) { lastPostTimeCache.Remove(channelId) - lastPostsCache.Remove(channelId) + + // Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60 + lastPostsCache.Remove(channelId + "30") + lastPostsCache.Remove(channelId + "60") } func (s SqlPostStore) GetEtag(channelId string, allowFromCache bool) store.StoreChannel { @@ -439,8 +442,9 @@ func (s SqlPostStore) GetPosts(channelId string, offset int, limit int, allowFro return } - if allowFromCache && offset == 0 && limit == 60 { - if cacheItem, ok := lastPostsCache.Get(channelId); ok { + // Caching only occurs on limits of 30 and 60, the common limits requested by MM clients + if allowFromCache && offset == 0 && (limit == 60 || limit == 30) { + if cacheItem, ok := lastPostsCache.Get(fmt.Sprintf("%s%v", channelId, limit)); ok { if s.metrics != nil { s.metrics.IncrementMemCacheHitCounter("Last Posts Cache") } @@ -482,8 +486,9 @@ func (s SqlPostStore) GetPosts(channelId string, offset int, limit int, allowFro list.MakeNonNil() - if offset == 0 && limit == 60 { - lastPostsCache.AddWithExpiresInSecs(channelId, list, LAST_POSTS_CACHE_SEC) + // Caching only occurs on limits of 30 and 60, the common limits requested by MM clients + if offset == 0 && (limit == 60 || limit == 30) { + lastPostsCache.AddWithExpiresInSecs(fmt.Sprintf("%s%v", channelId, limit), list, LAST_POSTS_CACHE_SEC) } result.Data = list diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 0de91f28b..56fdf9d6c 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_4_8_0 = "4.8.0" VERSION_4_7_0 = "4.7.0" VERSION_4_6_0 = "4.6.0" VERSION_4_5_0 = "4.5.0" @@ -64,6 +65,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion45(sqlStore) UpgradeDatabaseToVersion46(sqlStore) UpgradeDatabaseToVersion47(sqlStore) + UpgradeDatabaseToVersion48(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -343,6 +345,14 @@ func UpgradeDatabaseToVersion46(sqlStore SqlStore) { func UpgradeDatabaseToVersion47(sqlStore SqlStore) { if shouldPerformUpgrade(sqlStore, VERSION_4_6_0, VERSION_4_7_0) { sqlStore.AlterColumnTypeIfExists("Users", "Position", "varchar(128)", "varchar(128)") + sqlStore.AlterColumnTypeIfExists("OAuthAuthData", "State", "varchar(1024)", "varchar(1024)") saveSchemaVersion(sqlStore, VERSION_4_7_0) } } + +func UpgradeDatabaseToVersion48(sqlStore SqlStore) { + //TODO: Uncomment the following condition when version 4.8.0 is released + //if shouldPerformUpgrade(sqlStore, VERSION_4_7_0, VERSION_4_8_0) { + // saveSchemaVersion(sqlStore, VERSION_4_8_0) + //} +} diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 4deb7f8d4..e663d5a41 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -27,7 +27,7 @@ func TestPostStore(t *testing.T, ss store.Store) { t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, ss) }) t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, ss) }) t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, ss) }) - t.Run("GetPostsWtihDetails", func(t *testing.T) { testPostStoreGetPostsWtihDetails(t, ss) }) + t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, ss) }) t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) }) t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) }) t.Run("Search", func(t *testing.T) { testPostStoreSearch(t, ss) }) @@ -490,7 +490,7 @@ func testPostStoreGetWithChildren(t *testing.T, ss store.Store) { } } -func testPostStoreGetPostsWtihDetails(t *testing.T, ss store.Store) { +func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { o1 := &model.Post{} o1.ChannelId = model.NewId() o1.UserId = model.NewId() @@ -591,6 +591,25 @@ func testPostStoreGetPostsWtihDetails(t *testing.T, ss store.Store) { if r2.Posts[o1.Id].Message != o1.Message { t.Fatal("Missing parent") } + + // Run once to fill cache + <-ss.Post().GetPosts(o1.ChannelId, 0, 30, true) + + o6 := &model.Post{} + o6.ChannelId = o1.ChannelId + o6.UserId = model.NewId() + o6.Message = "zz" + model.NewId() + "b" + o6 = (<-ss.Post().Save(o6)).Data.(*model.Post) + + // Should only be 6 since we hit the cache + r3 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList) + assert.Equal(t, 6, len(r3.Order)) + + ss.Post().InvalidateLastPostTimeCache(o1.ChannelId) + + // Cache was invalidated, we should get all the posts + r4 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList) + assert.Equal(t, 7, len(r4.Order)) } func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { diff --git a/utils/api.go b/utils/api.go index 005c3284b..51524074d 100644 --- a/utils/api.go +++ b/utils/api.go @@ -4,6 +4,9 @@ package utils import ( + "crypto" + "crypto/rand" + "encoding/base64" "fmt" "html/template" "net/http" @@ -32,13 +35,25 @@ func OriginChecker(allowedOrigins string) func(*http.Request) bool { } } -func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { - status := http.StatusTemporaryRedirect - if err.StatusCode != http.StatusInternalServerError { - status = err.StatusCode +func RenderWebAppError(w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) { + RenderWebError(w, r, err.StatusCode, url.Values{ + "message": []string{err.Message}, + }, s) +} + +func RenderWebError(w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) { + queryString := params.Encode() + + h := crypto.SHA256 + sum := h.New() + sum.Write([]byte("/error?" + queryString)) + signature, err := s.Sign(rand.Reader, sum.Sum(nil), h) + if err != nil { + http.Error(w, "", http.StatusInternalServerError) + return } + destination := strings.TrimRight(GetSiteURL(), "/") + "/error?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature) - destination := strings.TrimRight(GetSiteURL(), "/") + "/error?message=" + url.QueryEscape(err.Message) if status >= 300 && status < 400 { http.Redirect(w, r, destination, status) return diff --git a/utils/api_test.go b/utils/api_test.go new file mode 100644 index 000000000..5e41c7bfe --- /dev/null +++ b/utils/api_test.go @@ -0,0 +1,49 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "encoding/base64" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRenderWebError(t *testing.T) { + r := httptest.NewRequest("GET", "http://foo", nil) + w := httptest.NewRecorder() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + RenderWebError(w, r, http.StatusTemporaryRedirect, url.Values{ + "foo": []string{"bar"}, + }, key) + + resp := w.Result() + location, err := url.Parse(resp.Header.Get("Location")) + require.NoError(t, err) + require.NotEmpty(t, location.Query().Get("s")) + + type ecdsaSignature struct { + R, S *big.Int + } + var rs ecdsaSignature + s, err := base64.URLEncoding.DecodeString(location.Query().Get("s")) + require.NoError(t, err) + _, err = asn1.Unmarshal(s, &rs) + require.NoError(t, err) + + assert.Equal(t, "bar", location.Query().Get("foo")) + h := sha256.Sum256([]byte("/error?foo=bar")) + assert.True(t, ecdsa.Verify(&key.PublicKey, h[:], rs.R, rs.S)) +} diff --git a/utils/config.go b/utils/config.go index be20e0367..10ada1728 100644 --- a/utils/config.go +++ b/utils/config.go @@ -342,7 +342,7 @@ func LoadConfig(fileName string) (config *model.Config, configPath string, appEr return config, configPath, nil } -func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]string { +func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string { props := make(map[string]string) props["Version"] = model.CurrentVersion @@ -456,18 +456,20 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable) - if IsLicensed() { - License := License() + hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != "" + props["HasImageProxy"] = strconv.FormatBool(hasImageProxy) + + if license != nil { props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) - if *License.Features.CustomBrand { + if *license.Features.CustomBrand { props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) props["CustomBrandText"] = *c.TeamSettings.CustomBrandText props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText } - if *License.Features.LDAP { + if *license.Features.LDAP { props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable) props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "") @@ -478,16 +480,16 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor } - if *License.Features.MFA { + if *license.Features.MFA { props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication) } - if *License.Features.Compliance { + if *license.Features.Compliance { props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable) } - if *License.Features.SAML { + if *license.Features.SAML { props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable) props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "") @@ -498,23 +500,23 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor } - if *License.Features.Cluster { + if *license.Features.Cluster { props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable) } - if *License.Features.Cluster { + if *license.Features.Cluster { props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable) } - if *License.Features.GoogleOAuth { + if *license.Features.GoogleOAuth { props["EnableSignUpWithGoogle"] = strconv.FormatBool(c.GoogleSettings.Enable) } - if *License.Features.Office365OAuth { + if *license.Features.Office365OAuth { props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable) } - if *License.Features.PasswordRequirements { + if *license.Features.PasswordRequirements { props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength) props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase) props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase) @@ -522,7 +524,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol) } - if *License.Features.Announcement { + if *license.Features.Announcement { props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner) props["BannerText"] = *c.AnnouncementSettings.BannerText props["BannerColor"] = *c.AnnouncementSettings.BannerColor @@ -530,14 +532,14 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal) } - if *License.Features.ThemeManagement { + if *license.Features.ThemeManagement { props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection) props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes) props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",") } - if *License.Features.DataRetention { + if *license.Features.DataRetention { props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion) props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10) props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion) diff --git a/utils/config_test.go b/utils/config_test.go index 9abc56d5e..5809422f1 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -197,7 +197,7 @@ func TestGetClientConfig(t *testing.T) { cfg, _, err := LoadConfig("config.json") require.Nil(t, err) - configMap := GenerateClientConfig(cfg, "") + configMap := GenerateClientConfig(cfg, "", nil) if configMap["EmailNotificationContentsType"] != *cfg.EmailSettings.EmailNotificationContentsType { t.Fatal("EmailSettings.EmailNotificationContentsType not exposed to client config") } diff --git a/utils/file_backend.go b/utils/file_backend.go index c7a6c5591..42af7f604 100644 --- a/utils/file_backend.go +++ b/utils/file_backend.go @@ -22,7 +22,7 @@ type FileBackend interface { RemoveDirectory(path string) *model.AppError } -func NewFileBackend(settings *model.FileSettings) (FileBackend, *model.AppError) { +func NewFileBackend(settings *model.FileSettings, enableComplianceFeatures bool) (FileBackend, *model.AppError) { switch *settings.DriverName { case model.IMAGE_DRIVER_S3: return &S3FileBackend{ @@ -33,7 +33,7 @@ func NewFileBackend(settings *model.FileSettings) (FileBackend, *model.AppError) signV2: settings.AmazonS3SignV2 != nil && *settings.AmazonS3SignV2, region: settings.AmazonS3Region, bucket: settings.AmazonS3Bucket, - encrypt: settings.AmazonS3SSE != nil && *settings.AmazonS3SSE && IsLicensed() && *License().Features.Compliance, + encrypt: settings.AmazonS3SSE != nil && *settings.AmazonS3SSE && enableComplianceFeatures, trace: settings.AmazonS3Trace != nil && *settings.AmazonS3Trace, }, nil case model.IMAGE_DRIVER_LOCAL: diff --git a/utils/file_backend_test.go b/utils/file_backend_test.go index 76cd1f4a8..46f75574e 100644 --- a/utils/file_backend_test.go +++ b/utils/file_backend_test.go @@ -63,7 +63,7 @@ func TestS3FileBackendTestSuite(t *testing.T) { func (s *FileBackendTestSuite) SetupTest() { TranslationsPreInit() - backend, err := NewFileBackend(&s.settings) + backend, err := NewFileBackend(&s.settings, true) require.Nil(s.T(), err) s.backend = backend } diff --git a/utils/license.go b/utils/license.go index 2aaa2a549..2853a58d0 100644 --- a/utils/license.go +++ b/utils/license.go @@ -5,28 +5,21 @@ package utils import ( "crypto" - "crypto/md5" "crypto/rsa" "crypto/sha512" "crypto/x509" "encoding/base64" "encoding/pem" - "fmt" "io/ioutil" "os" "strconv" "strings" - "sync/atomic" l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/model" ) -var isLicensedInt32 int32 -var licenseValue atomic.Value -var clientLicenseValue atomic.Value - var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N @@ -37,92 +30,6 @@ a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt hwIDAQAB -----END PUBLIC KEY-----`) -func init() { - SetLicense(nil) -} - -func IsLicensed() bool { - return atomic.LoadInt32(&isLicensedInt32) == 1 -} - -func SetIsLicensed(v bool) { - if v { - atomic.StoreInt32(&isLicensedInt32, 1) - } else { - atomic.StoreInt32(&isLicensedInt32, 0) - } -} - -func License() *model.License { - return licenseValue.Load().(*model.License) -} - -func SetClientLicense(m map[string]string) { - clientLicenseValue.Store(m) -} - -func ClientLicense() map[string]string { - return clientLicenseValue.Load().(map[string]string) -} - -func LoadLicense(licenseBytes []byte) { - if success, licenseStr := ValidateLicense(licenseBytes); success { - license := model.LicenseFromJson(strings.NewReader(licenseStr)) - SetLicense(license) - return - } - - l4g.Warn(T("utils.license.load_license.invalid.warn")) -} - -var licenseListeners = map[string]func(){} - -func AddLicenseListener(listener func()) string { - id := model.NewId() - licenseListeners[id] = listener - return id -} - -func RemoveLicenseListener(id string) { - delete(licenseListeners, id) -} - -func SetLicense(license *model.License) bool { - defer func() { - for _, listener := range licenseListeners { - listener() - } - }() - - if license == nil { - SetIsLicensed(false) - license = &model.License{ - Features: new(model.Features), - } - license.Features.SetDefaults() - licenseValue.Store(license) - - SetClientLicense(map[string]string{"IsLicensed": "false"}) - - return false - } else { - license.Features.SetDefaults() - - if !license.IsExpired() { - licenseValue.Store(license) - SetIsLicensed(true) - clientLicenseValue.Store(getClientLicense(license)) - return true - } - - return false - } -} - -func RemoveLicense() { - SetLicense(nil) -} - func ValidateLicense(signed []byte) (bool, string) { decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed))) @@ -213,12 +120,12 @@ func GetLicenseFileLocation(fileLocation string) string { } } -func getClientLicense(l *model.License) map[string]string { +func GetClientLicense(l *model.License) map[string]string { props := make(map[string]string) - props["IsLicensed"] = strconv.FormatBool(IsLicensed()) + props["IsLicensed"] = strconv.FormatBool(l != nil) - if IsLicensed() { + if l != nil { props["Id"] = l.Id props["Users"] = strconv.Itoa(*l.Features.Users) props["LDAP"] = strconv.FormatBool(*l.Features.LDAP) @@ -248,39 +155,3 @@ func getClientLicense(l *model.License) map[string]string { return props } - -func GetClientLicenseEtag(useSanitized bool) string { - value := "" - - lic := ClientLicense() - - if useSanitized { - lic = GetSanitizedClientLicense() - } - - for k, v := range lic { - value += fmt.Sprintf("%s:%s;", k, v) - } - - return model.Etag(fmt.Sprintf("%x", md5.Sum([]byte(value)))) -} - -func GetSanitizedClientLicense() map[string]string { - sanitizedLicense := make(map[string]string) - - for k, v := range ClientLicense() { - sanitizedLicense[k] = v - } - - if IsLicensed() { - delete(sanitizedLicense, "Id") - delete(sanitizedLicense, "Name") - delete(sanitizedLicense, "Email") - delete(sanitizedLicense, "PhoneNumber") - delete(sanitizedLicense, "IssuedAt") - delete(sanitizedLicense, "StartsAt") - delete(sanitizedLicense, "ExpiresAt") - } - - return sanitizedLicense -} diff --git a/utils/license_test.go b/utils/license_test.go index 9771ec497..c2d1b4c05 100644 --- a/utils/license_test.go +++ b/utils/license_test.go @@ -5,87 +5,20 @@ package utils import ( "testing" - - "github.com/mattermost/mattermost-server/model" ) -func TestSetLicense(t *testing.T) { - l1 := &model.License{} - l1.Features = &model.Features{} - l1.Customer = &model.Customer{} - l1.StartsAt = model.GetMillis() - 1000 - l1.ExpiresAt = model.GetMillis() + 100000 - if ok := SetLicense(l1); !ok { - t.Fatal("license should have worked") - } - - l2 := &model.License{} - l2.Features = &model.Features{} - l2.Customer = &model.Customer{} - l2.StartsAt = model.GetMillis() - 1000 - l2.ExpiresAt = model.GetMillis() - 100 - if ok := SetLicense(l2); ok { - t.Fatal("license should have failed") - } - - l3 := &model.License{} - l3.Features = &model.Features{} - l3.Customer = &model.Customer{} - l3.StartsAt = model.GetMillis() + 10000 - l3.ExpiresAt = model.GetMillis() + 100000 - if ok := SetLicense(l3); !ok { - t.Fatal("license should have passed") - } -} - func TestValidateLicense(t *testing.T) { b1 := []byte("junk") if ok, _ := ValidateLicense(b1); ok { t.Fatal("should have failed - bad license") } - LoadLicense(b1) - b2 := []byte("junkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunk") if ok, _ := ValidateLicense(b2); ok { t.Fatal("should have failed - bad license") } } -func TestClientLicenseEtag(t *testing.T) { - etag1 := GetClientLicenseEtag(false) - - SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) - - etag2 := GetClientLicenseEtag(false) - if etag1 == etag2 { - t.Fatal("etags should not match") - } - - SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "false"}) - - etag3 := GetClientLicenseEtag(false) - if etag2 == etag3 { - t.Fatal("etags should not match") - } -} - -func TestGetSanitizedClientLicense(t *testing.T) { - l1 := &model.License{} - l1.Features = &model.Features{} - l1.Customer = &model.Customer{} - l1.Customer.Name = "TestName" - l1.StartsAt = model.GetMillis() - 1000 - l1.ExpiresAt = model.GetMillis() + 100000 - SetLicense(l1) - - m := GetSanitizedClientLicense() - - if _, ok := m["Name"]; ok { - t.Fatal("should have been sanatized") - } -} - func TestGetLicenseFileLocation(t *testing.T) { fileName := GetLicenseFileLocation("") if len(fileName) == 0 { diff --git a/utils/mail.go b/utils/mail.go index 4c8a505af..633f97818 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -105,17 +105,17 @@ func TestConnection(config *model.Config) { defer c.Close() } -func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *model.AppError { +func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError { fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail} - return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config) + return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config, enableComplianceFeatures) } // allows for sending an email with attachments and differing MIME/SMTP recipients -func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError { - return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config) +func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { + return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config, enableComplianceFeatures) } -func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError { +func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 { return nil } @@ -151,7 +151,7 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string m.AddAlternative("text/html", htmlMessage) if attachments != nil { - fileBackend, err := NewFileBackend(&config.FileSettings) + fileBackend, err := NewFileBackend(&config.FileSettings, enableComplianceFeatures) if err != nil { return err } diff --git a/utils/mail_test.go b/utils/mail_test.go index 207fe32a5..703420441 100644 --- a/utils/mail_test.go +++ b/utils/mail_test.go @@ -50,7 +50,7 @@ func TestSendMailUsingConfig(t *testing.T) { //Delete all the messages before check the sample email DeleteMailBox(emailTo) - if err := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg); err != nil { + if err := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true); err != nil { t.Log(err) t.Fatal("Should connect to the STMP Server") } else { @@ -95,7 +95,7 @@ func TestSendMailUsingConfigAdvanced(t *testing.T) { DeleteMailBox(smtpTo) // create a file that will be attached to the email - fileBackend, err := NewFileBackend(&cfg.FileSettings) + fileBackend, err := NewFileBackend(&cfg.FileSettings, true) assert.Nil(t, err) fileContents := []byte("hello world") fileName := "file.txt" @@ -111,7 +111,7 @@ func TestSendMailUsingConfigAdvanced(t *testing.T) { headers := make(map[string]string) headers["TestHeader"] = "TestValue" - if err := SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, emailSubject, emailBody, attachments, headers, cfg); err != nil { + if err := SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, emailSubject, emailBody, attachments, headers, cfg, true); err != nil { t.Log(err) t.Fatal("Should connect to the STMP Server") } else { diff --git a/web/web.go b/web/web.go index 321d83a75..e0edd1b7a 100644 --- a/web/web.go +++ b/web/web.go @@ -94,7 +94,7 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { } if api.IsApiCall(r) { - api.Handle404(w, r) + api.Handle404(c.App, w, r) return } |