Merge branch 'master' into advanced-permissions-phase-1

This commit is contained in:
George Goldberg
2018-02-13 13:35:52 +00:00
74 changed files with 939 additions and 666 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"})
}
}

View File

@@ -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 {

View File

@@ -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()))

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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))

View File

@@ -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()

View File

@@ -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
}

22
api4/image.go Normal file
View File

@@ -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)
}
}

52
api4/image_test.go Normal file
View File

@@ -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"))
}

View File

@@ -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
}

View File

@@ -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"}}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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{

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"])
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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))

View File

@@ -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",

View File

@@ -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 })

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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",

View File

@@ -2202,10 +2202,6 @@
"id": "api.team.create_team_from_signup.expired_link.app_error",
"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."
@@ -2714,6 +2710,10 @@
"id": "api.user.create_user.signup_link_expired.app_error",
"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"
@@ -7330,10 +7330,6 @@
"id": "web.root.singup_title",
"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"
@@ -7350,10 +7346,6 @@
"id": "web.signup_user_complete.link_expired.app_error",
"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"

View File

@@ -5404,7 +5404,7 @@
},
{
"id": "model.user.is_valid.position.app_error",
"translation": "不正な役職: 128文字以上にはできません"
"translation": "不正な役職: 128文字以下でなければなりません"
},
{
"id": "model.user.is_valid.pwd.app_error",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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)

58
model/client4_test.go Normal file
View File

@@ -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)
}

View File

@@ -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)

View File

@@ -122,12 +122,13 @@ type PostActionIntegrationResponse struct {
func (o *Post) ToJson() string {
copy := *o
copy.StripActionIntegrations()
b, err := json.Marshal(&copy)
if err != nil {
return ""
} else {
return string(b)
}
b, _ := json.Marshal(&copy)
return string(b)
}
func (o *Post) ToUnsanitizedJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func PostFromJson(data io.Reader) *Post {

View File

@@ -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"`
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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)
//}
}

View File

@@ -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) {

View File

@@ -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

49
utils/api_test.go Normal file
View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}