summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile14
-rw-r--r--api/emoji_test.go2
-rw-r--r--api/user.go135
-rw-r--r--api/webhook_test.go154
-rw-r--r--api4/api.go22
-rw-r--r--api4/apitestlib.go2
-rw-r--r--api4/command_test.go187
-rw-r--r--api4/emoji.go3
-rw-r--r--api4/handlers.go67
-rw-r--r--api4/system_test.go4
-rw-r--r--api4/user.go8
-rw-r--r--api4/webhook.go91
-rw-r--r--api4/webhook_test.go45
-rw-r--r--app/command.go8
-rw-r--r--app/emoji.go9
-rw-r--r--app/file.go13
-rw-r--r--app/ldap.go6
-rw-r--r--app/login.go70
-rw-r--r--app/team.go2
-rw-r--r--app/user.go36
-rw-r--r--cmd/commands/ldap.go29
-rw-r--r--cmd/commands/server.go2
-rw-r--r--config/default.json1
-rw-r--r--einterfaces/ldap.go7
-rw-r--r--i18n/en.json8
-rw-r--r--mlog/log.go1
-rw-r--r--model/channel.go6
-rw-r--r--model/config.go11
-rw-r--r--model/switch_request.go2
-rw-r--r--model/websocket_client.go55
-rw-r--r--store/sqlstore/channel_store.go41
-rw-r--r--store/sqlstore/upgrade.go9
-rw-r--r--store/sqlstore/user_store.go6
-rw-r--r--store/store.go2
-rw-r--r--store/storetest/channel_store.go33
-rw-r--r--store/storetest/mocks/AuditStore.go2
-rw-r--r--store/storetest/mocks/ChannelMemberHistoryStore.go2
-rw-r--r--store/storetest/mocks/ChannelStore.go2
-rw-r--r--store/storetest/mocks/ClusterDiscoveryStore.go2
-rw-r--r--store/storetest/mocks/CommandStore.go2
-rw-r--r--store/storetest/mocks/CommandWebhookStore.go2
-rw-r--r--store/storetest/mocks/ComplianceStore.go2
-rw-r--r--store/storetest/mocks/EmojiStore.go2
-rw-r--r--store/storetest/mocks/FileInfoStore.go2
-rw-r--r--store/storetest/mocks/JobStore.go2
-rw-r--r--store/storetest/mocks/LayeredStoreDatabaseLayer.go2
-rw-r--r--store/storetest/mocks/LayeredStoreSupplier.go2
-rw-r--r--store/storetest/mocks/LicenseStore.go2
-rw-r--r--store/storetest/mocks/OAuthStore.go2
-rw-r--r--store/storetest/mocks/PluginStore.go2
-rw-r--r--store/storetest/mocks/PostStore.go2
-rw-r--r--store/storetest/mocks/PreferenceStore.go2
-rw-r--r--store/storetest/mocks/ReactionStore.go2
-rw-r--r--store/storetest/mocks/RoleStore.go2
-rw-r--r--store/storetest/mocks/SessionStore.go2
-rw-r--r--store/storetest/mocks/SqlStore.go2
-rw-r--r--store/storetest/mocks/StatusStore.go2
-rw-r--r--store/storetest/mocks/Store.go2
-rw-r--r--store/storetest/mocks/SystemStore.go2
-rw-r--r--store/storetest/mocks/TeamStore.go2
-rw-r--r--store/storetest/mocks/TokenStore.go2
-rw-r--r--store/storetest/mocks/UserAccessTokenStore.go2
-rw-r--r--store/storetest/mocks/UserStore.go12
-rw-r--r--store/storetest/mocks/WebhookStore.go2
-rw-r--r--store/storetest/user_store.go46
-rw-r--r--utils/file_backend.go3
-rw-r--r--utils/file_backend_local.go28
-rw-r--r--utils/file_backend_s3.go14
-rw-r--r--utils/file_backend_test.go67
-rw-r--r--utils/mail_test.go6
-rw-r--r--web/context.go (renamed from api4/context.go)170
-rw-r--r--web/context_test.go (renamed from api4/context_test.go)6
-rw-r--r--web/handlers.go158
-rw-r--r--web/params.go (renamed from api4/params.go)8
-rw-r--r--web/saml.go149
-rw-r--r--web/static.go84
-rw-r--r--web/web.go96
-rw-r--r--web/web_test.go99
-rw-r--r--web/webhook.go101
-rw-r--r--web/webhook_test.go216
80 files changed, 1306 insertions, 1104 deletions
diff --git a/Makefile b/Makefile
index 8f4b70fac..187b8f769 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ else
endif
# Golang Flags
-GOPATH ?= $(GOPATH:):./vendor
+GOPATH ?= $(shell go env GOPATH)
GOFLAGS ?= $(GOFLAGS:)
GO=go
GO_LINKER_FLAGS ?= -ldflags \
@@ -198,9 +198,9 @@ stop-docker: ## Stops the docker containers for local development.
fi
@if [ $(shell docker ps -a | grep -ci mattermost-minio) -eq 1 ]; then \
- echo stopping mattermost-minio; \
- docker stop mattermost-minio > /dev/null; \
- fi
+ echo stopping mattermost-minio; \
+ docker stop mattermost-minio > /dev/null; \
+ fi
@if [ $(shell docker ps -a | grep -ci mattermost-elasticsearch) -eq 1 ]; then \
echo stopping mattermost-elasticsearch; \
@@ -273,7 +273,11 @@ gofmt: ## Runs gofmt against all packages.
store-mocks: ## Creates mock files.
go get github.com/vektra/mockery/...
- 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`.'
+ $(GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.'
+
+ldap-mocks: ## Creates mock files for ldap.
+ go get github.com/vektra/mockery/...
+ GOPATH=$(shell go env GOPATH) $(shell go env GOPATH)/bin/mockery -dir enterprise/ldap -all -output enterprise/ldap/mocks -note 'Regenerate this file using `make ldap-mocks`.'
update-jira-plugin: ## Updates Jira plugin.
go get github.com/mattermost/go-bindata/...
diff --git a/api/emoji_test.go b/api/emoji_test.go
index 69922a54a..108c416e1 100644
--- a/api/emoji_test.go
+++ b/api/emoji_test.go
@@ -272,7 +272,7 @@ func TestDeleteEmoji(t *testing.T) {
func createTestEmoji(t *testing.T, a *app.App, emoji *model.Emoji, imageData []byte) *model.Emoji {
emoji = store.Must(a.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
- if err := a.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
+ if _, err := a.WriteFile(bytes.NewReader(imageData), "emoji/"+emoji.Id+"/image"); err != nil {
store.Must(a.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
t.Fatalf("failed to write image: %v", err.Error())
}
diff --git a/api/user.go b/api/user.go
index 15fd4c7ea..7592d1119 100644
--- a/api/user.go
+++ b/api/user.go
@@ -4,11 +4,9 @@
package api
import (
- b64 "encoding/base64"
"fmt"
"net/http"
"strconv"
- "strings"
"time"
"github.com/gorilla/mux"
@@ -62,9 +60,6 @@ func (api *API) InitUser() {
api.BaseRoutes.NeedUser.Handle("/audits", api.ApiUserRequired(getAudits)).Methods("GET")
api.BaseRoutes.NeedUser.Handle("/image", api.ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET")
api.BaseRoutes.NeedUser.Handle("/update_roles", api.ApiUserRequired(updateRoles)).Methods("POST")
-
- api.BaseRoutes.Root.Handle("/login/sso/saml", api.AppHandlerIndependent(loginWithSaml)).Methods("GET")
- api.BaseRoutes.Root.Handle("/login/sso/saml", api.AppHandlerIndependent(completeSaml)).Methods("POST")
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -107,7 +102,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
ldapOnly := props["ldap_only"] == "true"
c.LogAudit("attempt - user_id=" + id + " login_id=" + loginId)
- user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
+ user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, ldapOnly)
if err != nil {
c.LogAudit("failure - user_id=" + id + " login_id=" + loginId)
c.Err = err
@@ -1072,7 +1067,7 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
}
rdata := map[string]string{}
- if user, err := c.App.GetUserForLogin(loginId, false); err != nil {
+ if user, err := c.App.GetUserForLogin("", loginId); err != nil {
rdata["mfa_required"] = "false"
} else {
rdata["mfa_required"] = strconv.FormatBool(user.MfaActive)
@@ -1080,132 +1075,6 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(rdata)))
}
-func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
- samlInterface := c.App.Saml
-
- if samlInterface == nil {
- c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
- return
- }
-
- teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
- if err != nil {
- c.Err = err
- return
- }
- action := r.URL.Query().Get("action")
- redirectTo := r.URL.Query().Get("redirect_to")
- relayProps := map[string]string{}
- relayState := ""
-
- if len(action) != 0 {
- relayProps["team_id"] = teamId
- relayProps["action"] = action
- if action == model.OAUTH_ACTION_EMAIL_TO_SSO {
- relayProps["email"] = r.URL.Query().Get("email")
- }
- }
-
- if len(redirectTo) != 0 {
- relayProps["redirect_to"] = redirectTo
- }
-
- if len(relayProps) > 0 {
- relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJson(relayProps)))
- }
-
- if data, err := samlInterface.BuildRequest(relayState); err != nil {
- c.Err = err
- return
- } else {
- w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
- http.Redirect(w, r, data.URL, http.StatusFound)
- }
-}
-
-func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
- samlInterface := c.App.Saml
-
- if samlInterface == nil {
- c.Err = model.NewAppError("completeSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
- return
- }
-
- //Validate that the user is with SAML and all that
- encodedXML := r.FormValue("SAMLResponse")
- relayState := r.FormValue("RelayState")
-
- relayProps := make(map[string]string)
- if len(relayState) > 0 {
- stateStr := ""
- if b, err := b64.StdEncoding.DecodeString(relayState); err != nil {
- c.Err = model.NewAppError("completeSaml", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error(), http.StatusFound)
- return
- } else {
- stateStr = string(b)
- }
- relayProps = model.MapFromJson(strings.NewReader(stateStr))
- }
-
- action := relayProps["action"]
- if user, err := samlInterface.DoLogin(encodedXML, relayProps); err != nil {
- if action == model.OAUTH_ACTION_MOBILE {
- err.Translate(c.T)
- w.Write([]byte(err.ToJson()))
- } else {
- c.Err = err
- c.Err.StatusCode = http.StatusFound
- }
- return
- } else {
- if err := c.App.CheckUserAllAuthenticationCriteria(user, ""); err != nil {
- c.Err = err
- c.Err.StatusCode = http.StatusFound
- return
- }
-
- switch action {
- case model.OAUTH_ACTION_SIGNUP:
- teamId := relayProps["team_id"]
- if len(teamId) > 0 {
- c.App.Go(func() {
- if err := c.App.AddUserToTeamByTeamId(teamId, user); err != nil {
- mlog.Error(err.Error())
- } else {
- c.App.AddDirectChannels(teamId, user)
- }
- })
- }
- case model.OAUTH_ACTION_EMAIL_TO_SSO:
- if err := c.App.RevokeAllSessions(user.Id); err != nil {
- c.Err = err
- return
- }
- c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
- c.App.Go(func() {
- if err := c.App.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
- mlog.Error(err.Error())
- }
- })
- }
- doLogin(c, w, r, user, "")
- if c.Err != nil {
- return
- }
-
- if val, ok := relayProps["redirect_to"]; ok {
- http.Redirect(w, r, c.GetSiteURLHeader()+val, http.StatusFound)
- return
- }
-
- if action == model.OAUTH_ACTION_MOBILE {
- ReturnStatusOK(w)
- } else {
- http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusFound)
- }
- }
-}
-
func sanitizeProfile(c *Context, user *model.User) *model.User {
options := c.App.Config().GetSanitizeOptions()
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 0b3073f83..c9ca7d783 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -4,8 +4,6 @@
package api
import (
- "fmt"
- "net/http"
"testing"
"github.com/mattermost/mattermost-server/model"
@@ -968,155 +966,3 @@ func TestRegenOutgoingHookToken(t *testing.T) {
t.Fatal("should have errored - webhooks turned off")
}
}
-
-func TestIncomingWebhooks(t *testing.T) {
- th := Setup().InitBasic().InitSystemAdmin()
- defer th.TearDown()
-
- Client := th.SystemAdminClient
- team := th.SystemAdminTeam
- channel1 := th.CreateChannel(Client, team)
- user2 := th.CreateUser(Client)
- th.LinkUserToTeam(user2, team)
-
- th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = true })
-
- hook := &model.IncomingWebhook{ChannelId: channel1.Id}
- hook = Client.Must(Client.CreateIncomingWebhook(hook)).Data.(*model.IncomingWebhook)
-
- url := "/hooks/" + hook.Id
- text := `this is a \"test\"
- that contains a newline and a tab`
-
- if _, err := Client.DoPost(url, "{\"text\":\"this is a test\"}", "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, "{\"text\":\""+text+"\"}", "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", channel1.Name), "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", channel1.Name), "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", user2.Username), "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, "payload={\"text\":\"this is a test\"}", "application/x-www-form-urlencoded"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, "payload={\"text\":\""+text+"\"}", "application/x-www-form-urlencoded"); err != nil {
- t.Fatal(err)
- }
-
- 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 not have failed -- ExperimentalTownSquareIsReadOnly is false and it's not a read only channel")
- }
-
- th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
- 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")
- }
-
- attachmentPayload := `{
- "text": "this is a test",
- "attachments": [
- {
- "fallback": "Required plain-text summary of the attachment.",
-
- "color": "#36a64f",
-
- "pretext": "Optional text that appears above the attachment block",
-
- "author_name": "Bobby Tables",
- "author_link": "http://flickr.com/bobby/",
- "author_icon": "http://flickr.com/icons/bobby.jpg",
-
- "title": "Slack API Documentation",
- "title_link": "https://api.slack.com/",
-
- "text": "Optional text that appears within the attachment",
-
- "fields": [
- {
- "title": "Priority",
- "value": "High",
- "short": false
- }
- ],
-
- "image_url": "http://my-website.com/path/to/image.jpg",
- "thumb_url": "http://example.com/path/to/thumb.png"
- }
- ]
- }`
-
- if _, err := Client.DoPost(url, attachmentPayload, "application/json"); err != nil {
- t.Fatal(err)
- }
-
- if _, err := Client.DoPost(url, "{\"text\":\"\"}", "application/json"); err == nil || err.StatusCode != http.StatusBadRequest {
- t.Fatal("should have failed - no text")
- }
-
- tooLongText := ""
- for i := 0; i < 8200; i++ {
- tooLongText += "a"
- }
-
- if _, err := Client.DoPost(url, "{\"text\":\""+tooLongText+"\"}", "application/json"); err != nil {
- t.Fatal(err)
- }
-
- attachmentPayload = `{
- "text": "this is a test",
- "attachments": [
- {
- "fallback": "Required plain-text summary of the attachment.",
-
- "color": "#36a64f",
-
- "pretext": "Optional text that appears above the attachment block",
-
- "author_name": "Bobby Tables",
- "author_link": "http://flickr.com/bobby/",
- "author_icon": "http://flickr.com/icons/bobby.jpg",
-
- "title": "Slack API Documentation",
- "title_link": "https://api.slack.com/",
-
- "text": "` + tooLongText + `",
-
- "fields": [
- {
- "title": "Priority",
- "value": "High",
- "short": false
- }
- ],
-
- "image_url": "http://my-website.com/path/to/image.jpg",
- "thumb_url": "http://example.com/path/to/thumb.png"
- }
- ]
- }`
-
- if _, err := Client.DoPost(url, attachmentPayload, "application/json"); err != nil {
- t.Fatal(err)
- }
-
- th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = false })
-
- if _, err := Client.DoPost(url, "{\"text\":\"this is a test\"}", "application/json"); err == nil {
- t.Fatal("should have failed - webhooks turned off")
- }
-}
diff --git a/api4/api.go b/api4/api.go
index d36c3e3ee..9172391dd 100644
--- a/api4/api.go
+++ b/api4/api.go
@@ -4,14 +4,12 @@
package api4
import (
- "fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/app"
- "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/utils"
+ "github.com/mattermost/mattermost-server/web"
_ "github.com/nicksnyder/go-i18n/i18n"
)
@@ -231,7 +229,7 @@ func Init(a *app.App, root *mux.Router, full bool) *API {
api.InitRole()
api.InitImage()
- root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404))
+ root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
// REMOVE CONDITION WHEN APIv3 REMOVED
if full {
@@ -241,18 +239,8 @@ func Init(a *app.App, root *mux.Router, full bool) *API {
return api
}
-func Handle404(w http.ResponseWriter, r *http.Request) {
- err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
-
- mlog.Debug(fmt.Sprintf("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)))
-
- w.WriteHeader(err.StatusCode)
- err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'."
- w.Write([]byte(err.ToJson()))
+func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
+ web.Handle404(api.App, w, r)
}
-func ReturnStatusOK(w http.ResponseWriter) {
- m := make(map[string]string)
- m[model.STATUS] = model.STATUS_OK
- w.Write([]byte(model.MapToJson(m)))
-}
+var ReturnStatusOK = web.ReturnStatusOK
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
index 48765687a..0ce334154 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -26,6 +26,7 @@ import (
"github.com/mattermost/mattermost-server/store/sqlstore"
"github.com/mattermost/mattermost-server/store/storetest"
"github.com/mattermost/mattermost-server/utils"
+ "github.com/mattermost/mattermost-server/web"
"github.com/mattermost/mattermost-server/wsapi"
s3 "github.com/minio/minio-go"
@@ -120,6 +121,7 @@ func setupTestHelper(enterprise bool) *TestHelper {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
Init(th.App, th.App.Srv.Router, true)
+ web.NewWeb(th.App, th.App.Srv.Router)
wsapi.Init(th.App, th.App.Srv.WebSocketRouter)
th.App.Srv.Store.MarkSystemRanUnitTests()
th.App.DoAdvancedPermissionsMigration()
diff --git a/api4/command_test.go b/api4/command_test.go
index 8c4ce5d50..0d37d7440 100644
--- a/api4/command_test.go
+++ b/api4/command_test.go
@@ -5,9 +5,13 @@ package api4
import (
"fmt"
- "strings"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
"testing"
+ "github.com/stretchr/testify/require"
+
"github.com/mattermost/mattermost-server/model"
)
@@ -392,7 +396,7 @@ func TestRegenToken(t *testing.T) {
}
}
-func TestExecuteCommand(t *testing.T) {
+func TestExecuteInvalidCommand(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
@@ -407,101 +411,170 @@ func TestExecuteCommand(t *testing.T) {
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
- th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" })
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
- postCmd := &model.Command{
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ rc := &model.CommandResponse{}
+
+ w.Write([]byte(rc.ToJson()))
+ }))
+ defer ts.Close()
+
+ getCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
- URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
- Method: model.COMMAND_METHOD_POST,
- Trigger: "postcommand",
+ URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
+ Method: model.COMMAND_METHOD_GET,
+ Trigger: "getcommand",
}
- if _, err := th.App.CreateCommand(postCmd); err != nil {
- t.Fatal("failed to create post command")
+ if _, err := th.App.CreateCommand(getCmd); err != nil {
+ t.Fatal("failed to create get command")
}
- commandResponse, resp := Client.ExecuteCommand(channel.Id, "/postcommand")
+ _, resp := Client.ExecuteCommand(channel.Id, "")
+ CheckBadRequestStatus(t, resp)
+
+ _, resp = Client.ExecuteCommand(channel.Id, "/")
+ CheckBadRequestStatus(t, resp)
+
+ _, resp = Client.ExecuteCommand(channel.Id, "getcommand")
+ CheckBadRequestStatus(t, resp)
+
+ _, resp = Client.ExecuteCommand(channel.Id, "/junk")
+ CheckNotFoundStatus(t, resp)
+
+ otherUser := th.CreateUser()
+ Client.Login(otherUser.Email, otherUser.Password)
+
+ _, resp = Client.ExecuteCommand(channel.Id, "/getcommand")
+ CheckForbiddenStatus(t, resp)
+
+ Client.Logout()
+
+ _, resp = Client.ExecuteCommand(channel.Id, "/getcommand")
+ CheckUnauthorizedStatus(t, resp)
+
+ _, resp = th.SystemAdminClient.ExecuteCommand(channel.Id, "/getcommand")
CheckNoError(t, resp)
+}
- if commandResponse == nil {
- t.Fatal("command response should have returned")
- }
+func TestExecuteGetCommand(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+ channel := th.BasicChannel
- posts, err := th.App.GetPostsPage(channel.Id, 0, 10)
- if err != nil || posts == nil || len(posts.Order) != 3 {
- t.Fatal("Test command failed to send")
+ enableCommands := *th.App.Config().ServiceSettings.EnableCommands
+ allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
+ })
+ }()
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
+
+ token := model.NewId()
+ expectedCommandResponse := &model.CommandResponse{
+ Text: "test get command response",
+ ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
+ Type: "custom_test",
+ Props: map[string]interface{}{"someprop": "somevalue"},
}
- cmdPosted := false
- for _, post := range posts.Posts {
- if strings.Contains(post.Message, "test command response") {
- if post.Type != "custom_test" {
- t.Fatal("wrong type set in slash command post")
- }
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
- if post.Props["someprop"] != "somevalue" {
- t.Fatal("wrong prop set in slash command post")
- }
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ require.NoError(t, err)
- cmdPosted = true
- break
- }
- }
+ require.Equal(t, token, values.Get("token"))
+ require.Equal(t, th.BasicTeam.Name, values.Get("team_domain"))
- if !cmdPosted {
- t.Fatal("Test command response failed to post")
- }
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(expectedCommandResponse.ToJson()))
+ }))
+ defer ts.Close()
getCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
- URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
+ URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
Method: model.COMMAND_METHOD_GET,
Trigger: "getcommand",
+ Token: token,
}
if _, err := th.App.CreateCommand(getCmd); err != nil {
t.Fatal("failed to create get command")
}
- commandResponse, resp = Client.ExecuteCommand(channel.Id, "/getcommand")
+ commandResponse, resp := Client.ExecuteCommand(channel.Id, "/getcommand")
CheckNoError(t, resp)
- if commandResponse == nil {
- t.Fatal("command response should have returned")
- }
+ expectedCommandResponse.Props["from_webhook"] = "true"
+ require.Equal(t, expectedCommandResponse, commandResponse)
+}
- posts, err = th.App.GetPostsPage(channel.Id, 0, 10)
- if err != nil || posts == nil || len(posts.Order) != 4 {
- t.Fatal("Test command failed to send")
- }
+func TestExecutePostCommand(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+ channel := th.BasicChannel
- _, resp = Client.ExecuteCommand(channel.Id, "")
- CheckBadRequestStatus(t, resp)
+ enableCommands := *th.App.Config().ServiceSettings.EnableCommands
+ allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
+ })
+ }()
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
- _, resp = Client.ExecuteCommand(channel.Id, "/")
- CheckBadRequestStatus(t, resp)
+ token := model.NewId()
+ expectedCommandResponse := &model.CommandResponse{
+ Text: "test post command response",
+ ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
+ Type: "custom_test",
+ Props: map[string]interface{}{"someprop": "somevalue"},
+ }
- _, resp = Client.ExecuteCommand(channel.Id, "getcommand")
- CheckBadRequestStatus(t, resp)
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodPost, r.Method)
- _, resp = Client.ExecuteCommand(channel.Id, "/junk")
- CheckNotFoundStatus(t, resp)
+ r.ParseForm()
- otherUser := th.CreateUser()
- Client.Login(otherUser.Email, otherUser.Password)
+ require.Equal(t, token, r.FormValue("token"))
+ require.Equal(t, th.BasicTeam.Name, r.FormValue("team_domain"))
- _, resp = Client.ExecuteCommand(channel.Id, "/getcommand")
- CheckForbiddenStatus(t, resp)
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(expectedCommandResponse.ToJson()))
+ }))
+ defer ts.Close()
- Client.Logout()
+ getCmd := &model.Command{
+ CreatorId: th.BasicUser.Id,
+ TeamId: th.BasicTeam.Id,
+ URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
+ Method: model.COMMAND_METHOD_POST,
+ Trigger: "postcommand",
+ Token: token,
+ }
- _, resp = Client.ExecuteCommand(channel.Id, "/getcommand")
- CheckUnauthorizedStatus(t, resp)
+ if _, err := th.App.CreateCommand(getCmd); err != nil {
+ t.Fatal("failed to create get command")
+ }
- _, resp = th.SystemAdminClient.ExecuteCommand(channel.Id, "/getcommand")
+ commandResponse, resp := Client.ExecuteCommand(channel.Id, "/postcommand")
CheckNoError(t, resp)
+
+ expectedCommandResponse.Props["from_webhook"] = "true"
+ require.Equal(t, expectedCommandResponse, commandResponse)
+
}
func TestExecuteCommandAgainstChannelOnAnotherTeam(t *testing.T) {
diff --git a/api4/emoji.go b/api4/emoji.go
index ae4a35dd5..cfb5dd6ab 100644
--- a/api4/emoji.go
+++ b/api4/emoji.go
@@ -9,6 +9,7 @@ import (
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/web"
)
const (
@@ -197,7 +198,7 @@ func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- emojis, err := c.App.SearchEmoji(emojiSearch.Term, emojiSearch.PrefixOnly, PER_PAGE_MAXIMUM)
+ emojis, err := c.App.SearchEmoji(emojiSearch.Term, emojiSearch.PrefixOnly, web.PER_PAGE_MAXIMUM)
if err != nil {
c.Err = err
return
diff --git a/api4/handlers.go b/api4/handlers.go
new file mode 100644
index 000000000..74e2fc88d
--- /dev/null
+++ b/api4/handlers.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+
+ "github.com/mattermost/mattermost-server/web"
+)
+
+type Context = web.Context
+
+func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &web.Handler{
+ App: api.App,
+ HandleFunc: h,
+ RequireSession: false,
+ TrustRequester: false,
+ RequireMfa: false,
+ IsStatic: false,
+ }
+}
+
+func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &web.Handler{
+ App: api.App,
+ HandleFunc: h,
+ RequireSession: true,
+ TrustRequester: false,
+ RequireMfa: true,
+ IsStatic: false,
+ }
+}
+
+func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &web.Handler{
+ App: api.App,
+ HandleFunc: h,
+ RequireSession: true,
+ TrustRequester: false,
+ RequireMfa: false,
+ IsStatic: false,
+ }
+}
+
+func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &web.Handler{
+ App: api.App,
+ HandleFunc: h,
+ RequireSession: false,
+ TrustRequester: true,
+ RequireMfa: false,
+ IsStatic: false,
+ }
+}
+
+func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &web.Handler{
+ App: api.App,
+ HandleFunc: h,
+ RequireSession: true,
+ TrustRequester: true,
+ RequireMfa: true,
+ IsStatic: false,
+ }
+}
diff --git a/api4/system_test.go b/api4/system_test.go
index 8e15f6ed2..f46ae7436 100644
--- a/api4/system_test.go
+++ b/api4/system_test.go
@@ -570,7 +570,7 @@ func TestGetAnalyticsOld(t *testing.T) {
CheckUnauthorizedStatus(t, resp)
}
-/*func TestS3TestConnection(t *testing.T) {
+func TestS3TestConnection(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
@@ -626,7 +626,7 @@ func TestGetAnalyticsOld(t *testing.T) {
_, resp = th.SystemAdminClient.TestS3Connection(&config)
CheckOKStatus(t, resp)
-}*/
+}
func TestSupportedTimezones(t *testing.T) {
th := Setup().InitBasic()
diff --git a/api4/user.go b/api4/user.go
index 897c49ad1..2a539a551 100644
--- a/api4/user.go
+++ b/api4/user.go
@@ -771,7 +771,7 @@ func checkUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if user, err := c.App.GetUserForLogin(loginId, false); err == nil {
+ if user, err := c.App.GetUserForLogin("", loginId); err == nil {
resp["mfa_required"] = user.MfaActive
}
@@ -943,7 +943,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
ldapOnly := props["ldap_only"] == "true"
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
- user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
+ user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, ldapOnly)
if err != nil {
c.LogAuditWithUserId(id, "failure - login_id="+loginId)
c.Err = err
@@ -1167,7 +1167,7 @@ func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- user, err := c.App.GetUserForLogin(email, false)
+ user, err := c.App.GetUserForLogin("", email)
if err != nil {
// Don't want to leak whether the email is valid or not
ReturnStatusOK(w)
@@ -1205,7 +1205,7 @@ func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) {
link, err = c.App.SwitchOAuthToEmail(switchRequest.Email, switchRequest.NewPassword, c.Session.UserId)
} else if switchRequest.EmailToLdap() {
- link, err = c.App.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapId, switchRequest.NewPassword)
+ link, err = c.App.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapLoginId, switchRequest.NewPassword)
} else if switchRequest.LdapToEmail() {
link, err = c.App.SwitchLdapToEmail(switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword)
} else {
diff --git a/api4/webhook.go b/api4/webhook.go
index fadc3fbf3..ec90c6f3a 100644
--- a/api4/webhook.go
+++ b/api4/webhook.go
@@ -4,14 +4,8 @@
package api4
import (
- "fmt"
- "io"
"net/http"
- "strings"
- "github.com/gorilla/mux"
- "github.com/gorilla/schema"
- "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
)
@@ -28,12 +22,6 @@ func (api *API) InitWebhook() {
api.BaseRoutes.OutgoingHook.Handle("", api.ApiSessionRequired(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.ApiSessionRequired(deleteOutgoingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHook.Handle("/regen_token", api.ApiSessionRequired(regenOutgoingHookToken)).Methods("POST")
-
- api.BaseRoutes.Root.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", api.ApiHandler(commandWebhook)).Methods("POST")
- api.BaseRoutes.Root.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiHandler(incomingWebhook)).Methods("POST")
-
- // Old endpoint for backwards compatibility
- api.BaseRoutes.Root.Handle("/api/v3/teams/{team_id:[A-Za-z0-9]+}/hooks/{id:[A-Za-z0-9]+}", api.ApiHandler(incomingWebhook)).Methods("POST")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -454,82 +442,3 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("success")
ReturnStatusOK(w)
}
-
-func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- id := params["id"]
-
- r.ParseForm()
-
- var err *model.AppError
- incomingWebhookPayload := &model.IncomingWebhookRequest{}
- contentType := r.Header.Get("Content-Type")
- if strings.Split(contentType, "; ")[0] == "application/x-www-form-urlencoded" {
- payload := strings.NewReader(r.FormValue("payload"))
-
- incomingWebhookPayload, err = decodePayload(payload)
- if err != nil {
- c.Err = err
- return
- }
- } else if strings.HasPrefix(contentType, "multipart/form-data") {
- r.ParseMultipartForm(0)
-
- decoder := schema.NewDecoder()
- err := decoder.Decode(incomingWebhookPayload, r.PostForm)
-
- if err != nil {
- c.Err = model.NewAppError("incomingWebhook", "api.webhook.incoming.error", nil, err.Error(), http.StatusBadRequest)
- return
- }
- } else {
- incomingWebhookPayload, err = decodePayload(r.Body)
- if err != nil {
- c.Err = err
- return
- }
- }
-
- if c.App.Config().LogSettings.EnableWebhookDebugging {
- mlog.Debug(fmt.Sprint("Incoming webhook received. Content=", incomingWebhookPayload.ToJson()))
- }
-
- err = c.App.HandleIncomingWebhook(id, incomingWebhookPayload)
- if err != nil {
- c.Err = err
- return
- }
-
- w.Header().Set("Content-Type", "text/plain")
- w.Write([]byte("ok"))
-}
-
-func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
- params := mux.Vars(r)
- id := params["id"]
-
- response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
- if err != nil {
- c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, err.Error(), http.StatusBadRequest)
- return
- }
-
- appErr := c.App.HandleCommandWebhook(id, response)
- if appErr != nil {
- c.Err = appErr
- return
- }
-
- w.Header().Set("Content-Type", "text/plain")
- w.Write([]byte("ok"))
-}
-
-func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) {
- incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJson(payload)
-
- if decodeError != nil {
- return nil, decodeError
- }
-
- return incomingWebhookPayload, nil
-}
diff --git a/api4/webhook_test.go b/api4/webhook_test.go
index e983b6461..441fb8bb7 100644
--- a/api4/webhook_test.go
+++ b/api4/webhook_test.go
@@ -4,8 +4,6 @@
package api4
import (
- "bytes"
- "net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -892,46 +890,3 @@ func TestDeleteOutgoingHook(t *testing.T) {
CheckForbiddenStatus(t, resp)
})
}
-
-func TestCommandWebhooks(t *testing.T) {
- th := Setup().InitBasic().InitSystemAdmin()
- defer th.TearDown()
-
- Client := th.SystemAdminClient
-
- cmd := &model.Command{
- CreatorId: th.BasicUser.Id,
- TeamId: th.BasicTeam.Id,
- URL: "http://nowhere.com",
- Method: model.COMMAND_METHOD_POST,
- Trigger: "delayed"}
-
- cmd, _ = Client.CreateCommand(cmd)
- args := &model.CommandArgs{
- TeamId: th.BasicTeam.Id,
- UserId: th.BasicUser.Id,
- ChannelId: th.BasicChannel.Id,
- }
- hook, err := th.App.CreateCommandWebhook(cmd.Id, args)
- if err != nil {
- t.Fatal(err)
- }
-
- if resp, _ := http.Post(Client.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound {
- t.Fatal("expected not-found for non-existent hook")
- }
-
- if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest {
- t.Fatal(err)
- }
-
- for i := 0; i < 5; i++ {
- if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK {
- t.Fatal(err)
- }
- }
-
- if resp, _ := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest {
- t.Fatal("expected error for sixth usage")
- }
-}
diff --git a/app/command.go b/app/command.go
index 796d656a7..92c35865a 100644
--- a/app/command.go
+++ b/app/command.go
@@ -230,12 +230,14 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
}
- method := "POST"
+ var req *http.Request
if cmd.Method == model.COMMAND_METHOD_GET {
- method = "GET"
+ req, _ = http.NewRequest(http.MethodGet, cmd.URL, nil)
+ req.URL.RawQuery = p.Encode()
+ } else {
+ req, _ = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
}
- req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode()))
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Method == model.COMMAND_METHOD_POST {
diff --git a/app/emoji.go b/app/emoji.go
index f48501cf9..b07331e65 100644
--- a/app/emoji.go
+++ b/app/emoji.go
@@ -23,7 +23,7 @@ import (
)
const (
- MaxEmojiFileSize = 1000 * 1024 // 1 MB
+ MaxEmojiFileSize = 1 << 20 // 1 MB
MaxEmojiWidth = 128
MaxEmojiHeight = 128
)
@@ -98,7 +98,7 @@ func (a *App) UploadEmojiImage(id string, imageData *multipart.FileHeader) *mode
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest)
}
- if err := a.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ if _, err := a.WriteFile(newbuf, getEmojiImagePath(id)); err != nil {
return err
}
}
@@ -110,14 +110,15 @@ func (a *App) UploadEmojiImage(id string, imageData *multipart.FileHeader) *mode
if err := png.Encode(newbuf, resized_image); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest)
}
- if err := a.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ if _, err := a.WriteFile(newbuf, getEmojiImagePath(id)); err != nil {
return err
}
}
}
}
- return a.WriteFile(buf.Bytes(), getEmojiImagePath(id))
+ _, appErr := a.WriteFile(buf, getEmojiImagePath(id))
+ return appErr
}
func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError {
diff --git a/app/file.go b/app/file.go
index 87e1986a2..cb8d54cb1 100644
--- a/app/file.go
+++ b/app/file.go
@@ -78,12 +78,13 @@ func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
return backend.MoveFile(oldPath, newPath)
}
-func (a *App) WriteFile(f []byte, path string) *model.AppError {
+func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
backend, err := a.FileBackend()
if err != nil {
- return err
+ return 0, err
}
- return backend.WriteFile(f, path)
+
+ return backend.WriteFile(fr, path)
}
func (a *App) RemoveFile(path string) *model.AppError {
@@ -414,7 +415,7 @@ func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string,
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
}
- if err := a.WriteFile(data, info.Path); err != nil {
+ if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
return nil, err
}
@@ -531,7 +532,7 @@ func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string, widt
return
}
- if err := a.WriteFile(buf.Bytes(), thumbnailPath); err != nil {
+ if _, err := a.WriteFile(buf, thumbnailPath); err != nil {
mlog.Error(fmt.Sprintf("Unable to upload thumbnail path=%v err=%v", thumbnailPath, err))
return
}
@@ -553,7 +554,7 @@ func (a *App) generatePreviewImage(img image.Image, previewPath string, width in
return
}
- if err := a.WriteFile(buf.Bytes(), previewPath); err != nil {
+ if _, err := a.WriteFile(buf, previewPath); err != nil {
mlog.Error(fmt.Sprintf("Unable to upload preview err=%v", err), mlog.String("path", previewPath))
return
}
diff --git a/app/ldap.go b/app/ldap.go
index 22c3b746b..544905b70 100644
--- a/app/ldap.go
+++ b/app/ldap.go
@@ -40,7 +40,7 @@ func (a *App) TestLdap() *model.AppError {
return nil
}
-func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword string) (string, *model.AppError) {
+func (a *App) SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError) {
if a.License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusForbidden)
}
@@ -63,7 +63,7 @@ func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword stri
return "", model.NewAppError("SwitchEmailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
}
- if err := ldapInterface.SwitchToLdap(user.Id, ldapId, ldapPassword); err != nil {
+ if err := ldapInterface.SwitchToLdap(user.Id, ldapLoginId, ldapPassword); err != nil {
return "", err
}
@@ -95,7 +95,7 @@ func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented)
}
- if err := ldapInterface.CheckPassword(*user.AuthData, ldapPassword); err != nil {
+ if err := ldapInterface.CheckPasswordAuthData(*user.AuthData, ldapPassword); err != nil {
return "", err
}
diff --git a/app/login.go b/app/login.go
index a2f06dbc3..a07056c1e 100644
--- a/app/login.go
+++ b/app/login.go
@@ -10,47 +10,69 @@ import (
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/store"
)
-func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) {
+func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
+ // Do statistics
+ defer func() {
+ if a.Metrics != nil {
+ if user == nil || err != nil {
+ a.Metrics.IncrementLoginFail()
+ } else {
+ a.Metrics.IncrementLogin()
+ }
+ }
+ }()
+
if len(password) == 0 {
err := model.NewAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "", http.StatusBadRequest)
return nil, err
}
- var user *model.User
- var err *model.AppError
+ // Get the MM user we are trying to login
+ if user, err = a.GetUserForLogin(id, loginId); err != nil {
+ return nil, err
+ }
+
+ // and then authenticate them
+ if user, err = a.authenticateUser(user, password, mfaToken); err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) {
+ enableUsername := *a.Config().EmailSettings.EnableSignInWithUsername
+ enableEmail := *a.Config().EmailSettings.EnableSignInWithEmail
+ // If we are given a userID then fail if we can't find a user with that ID
if len(id) != 0 {
- if user, err = a.GetUser(id); err != nil {
- err.StatusCode = http.StatusBadRequest
- if a.Metrics != nil {
- a.Metrics.IncrementLoginFail()
+ if user, err := a.GetUser(id); err != nil {
+ if err.Id != store.MISSING_ACCOUNT_ERROR {
+ err.StatusCode = http.StatusInternalServerError
+ return nil, err
+ } else {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
}
- return nil, err
- }
- } else {
- if user, err = a.GetUserForLogin(loginId, ldapOnly); err != nil {
- if a.Metrics != nil {
- a.Metrics.IncrementLoginFail()
- }
- return nil, err
+ } else {
+ return user, nil
}
}
- // and then authenticate them
- if user, err = a.authenticateUser(user, password, mfaToken); err != nil {
- if a.Metrics != nil {
- a.Metrics.IncrementLoginFail()
- }
- return nil, err
+ // Try to get the user by username/email
+ if result := <-a.Srv.Store.User().GetForLogin(loginId, enableUsername, enableEmail); result.Err == nil {
+ return result.Data.(*model.User), nil
}
- if a.Metrics != nil {
- a.Metrics.IncrementLogin()
+ // Try to get the user with LDAP
+ if user, err := a.Ldap.GetUser(loginId); err == nil {
+ return user, nil
}
- return user, nil
+ return nil, model.NewAppError("GetUserForLogin", "store.sql_user.get_for_login.app_error", nil, "", http.StatusBadRequest)
}
func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) (*model.Session, *model.AppError) {
diff --git a/app/team.go b/app/team.go
index f5235792f..aca99dd1e 100644
--- a/app/team.go
+++ b/app/team.go
@@ -1001,7 +1001,7 @@ func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.App
path := "teams/" + teamId + "/teamIcon.png"
- if err := a.WriteFile(buf.Bytes(), path); err != nil {
+ if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError)
}
diff --git a/app/user.go b/app/user.go
index fd8b6b377..2ee410684 100644
--- a/app/user.go
+++ b/app/user.go
@@ -382,38 +382,6 @@ func (a *App) GetUserByAuth(authData *string, authService string) (*model.User,
}
}
-func (a *App) GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) {
- license := a.License()
- ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && license != nil && *license.Features.LDAP
-
- if result := <-a.Srv.Store.User().GetForLogin(
- loginId,
- *a.Config().EmailSettings.EnableSignInWithUsername && !onlyLdap,
- *a.Config().EmailSettings.EnableSignInWithEmail && !onlyLdap,
- ldapAvailable,
- ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
- // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
- result.Err.StatusCode = http.StatusBadRequest
- return nil, result.Err
- } else if result.Err != nil {
- if !ldapAvailable {
- // failed to find user and no LDAP server to fall back on
- result.Err.StatusCode = http.StatusBadRequest
- return nil, result.Err
- }
-
- // fall back to LDAP server to see if we can find a user
- if ldapUser, ldapErr := a.Ldap.GetUser(loginId); ldapErr != nil {
- ldapErr.StatusCode = http.StatusBadRequest
- return nil, ldapErr
- } else {
- return ldapUser, nil
- }
- } else {
- return result.Data.(*model.User), nil
- }
-}
-
func (a *App) GetUsers(offset int, limit int) ([]*model.User, *model.AppError) {
if result := <-a.Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
return nil, result.Err
@@ -786,7 +754,7 @@ func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
}
if user.LastPictureUpdate == 0 {
- if err := a.WriteFile(img, path); err != nil {
+ if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
@@ -842,7 +810,7 @@ func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model
path := "users/" + userId + "/profile.png"
- if err := a.WriteFile(buf.Bytes(), path); err != nil {
+ if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "", http.StatusInternalServerError)
}
diff --git a/cmd/commands/ldap.go b/cmd/commands/ldap.go
index 0c79ce32b..03c366213 100644
--- a/cmd/commands/ldap.go
+++ b/cmd/commands/ldap.go
@@ -22,9 +22,19 @@ var LdapSyncCmd = &cobra.Command{
RunE: ldapSyncCmdF,
}
+var LdapIdMigrate = &cobra.Command{
+ Use: "idmigrate",
+ Short: "Migrate LDAP IdAttribute to new value",
+ Long: "Migrate LDAP IdAttribute to new value. Run this utility then change the IdAttribute to the new value.",
+ Example: " ldap idmigrate objectGUID",
+ Args: cobra.ExactArgs(1),
+ RunE: ldapIdMigrateCmdF,
+}
+
func init() {
LdapCmd.AddCommand(
LdapSyncCmd,
+ LdapIdMigrate,
)
cmd.RootCmd.AddCommand(LdapCmd)
}
@@ -47,3 +57,22 @@ func ldapSyncCmdF(command *cobra.Command, args []string) error {
return nil
}
+
+func ldapIdMigrateCmdF(command *cobra.Command, args []string) error {
+ a, err := cmd.InitDBCommandContextCobra(command)
+ if err != nil {
+ return err
+ }
+ defer a.Shutdown()
+
+ toAttribute := args[0]
+ if ldapI := a.Ldap; ldapI != nil {
+ if err := ldapI.MigrateIDAttribute(toAttribute); err != nil {
+ cmd.CommandPrintErrorln("ERROR: AD/LDAP IdAttribute migration failed! Error: " + err.Error())
+ } else {
+ cmd.CommandPrettyPrintln("SUCCESS: AD/LDAP IdAttribute migration complete. You can now change your IdAttribute to: " + toAttribute)
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/commands/server.go b/cmd/commands/server.go
index 77f195f4b..441eb82bc 100644
--- a/cmd/commands/server.go
+++ b/cmd/commands/server.go
@@ -107,7 +107,7 @@ func runServer(configFileLocation string, disableConfigWatch bool, interruptChan
api4.Init(a, a.Srv.Router, false)
api3 := api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
- web.Init(api3)
+ web.NewWeb(a, a.Srv.Router)
license := a.License()
diff --git a/config/default.json b/config/default.json
index 8bf06dc8b..80a694e38 100644
--- a/config/default.json
+++ b/config/default.json
@@ -263,6 +263,7 @@
"NicknameAttribute": "",
"IdAttribute": "",
"PositionAttribute": "",
+ "LoginIdAttribute": "",
"SyncIntervalMinutes": 60,
"SkipCertificateVerification": false,
"QueryTimeout": 60,
diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go
index 26326b174..31e8b7cf8 100644
--- a/einterfaces/ldap.go
+++ b/einterfaces/ldap.go
@@ -4,8 +4,6 @@
package einterfaces
import (
- "github.com/go-ldap/ldap"
-
"github.com/mattermost/mattermost-server/model"
)
@@ -14,12 +12,11 @@ type LdapInterface interface {
GetUser(id string) (*model.User, *model.AppError)
GetUserAttributes(id string, attributes []string) (map[string]string, *model.AppError)
CheckPassword(id string, password string) *model.AppError
+ CheckPasswordAuthData(authData string, password string) *model.AppError
SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError
ValidateFilter(filter string) *model.AppError
StartSynchronizeJob(waitForJobToFinish bool) (*model.Job, *model.AppError)
RunTest() *model.AppError
GetAllLdapUsers() ([]*model.User, *model.AppError)
- UserFromLdapUser(ldapUser *ldap.Entry) *model.User
- UserHasUpdateFromLdap(existingUser *model.User, currentLdapUser *model.User) bool
- UpdateLocalLdapUser(existingUser *model.User, currentLdapUser *model.User) *model.User
+ MigrateIDAttribute(toAttribute string) error
}
diff --git a/i18n/en.json b/i18n/en.json
index c66066fcf..0f3dacd95 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -5023,6 +5023,10 @@
"translation": "AD/LDAP field \"ID Attribute\" is required."
},
{
+ "id": "model.config.is_valid.ldap_login_id",
+ "translation": "AD/LDAP field \"Login ID Attribute\" is required."
+ },
+ {
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP field \"Last Name Attribute\" is required."
},
@@ -5927,10 +5931,6 @@
"translation": "We couldn't delete the channel"
},
{
- "id": "store.sql_channel.extra_updated.app_error",
- "translation": "Problem updating members last updated time"
- },
- {
"id": "store.sql_channel.get.existing.app_error",
"translation": "We couldn't find the existing channel"
},
diff --git a/mlog/log.go b/mlog/log.go
index 0d1e28e8d..c3261459b 100644
--- a/mlog/log.go
+++ b/mlog/log.go
@@ -29,6 +29,7 @@ type Field = zapcore.Field
var Int64 = zap.Int64
var Int = zap.Int
var String = zap.String
+var Any = zap.Any
var Err = zap.Error
type LoggerConfiguration struct {
diff --git a/model/channel.go b/model/channel.go
index df68202d6..749b8dc94 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -44,7 +44,6 @@ type Channel struct {
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
- ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
}
@@ -133,17 +132,12 @@ func (o *Channel) PreSave() {
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
- o.ExtraUpdateAt = o.CreateAt
}
func (o *Channel) PreUpdate() {
o.UpdateAt = GetMillis()
}
-func (o *Channel) ExtraUpdated() {
- o.ExtraUpdateAt = GetMillis()
-}
-
func (o *Channel) IsGroupOrDirect() bool {
return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP
}
diff --git a/model/config.go b/model/config.go
index 5074b7637..07cd9d977 100644
--- a/model/config.go
+++ b/model/config.go
@@ -1144,6 +1144,7 @@ type LdapSettings struct {
NicknameAttribute *string
IdAttribute *string
PositionAttribute *string
+ LoginIdAttribute *string
// Synchronization
SyncIntervalMinutes *int
@@ -1227,6 +1228,12 @@ func (s *LdapSettings) SetDefaults() {
s.PositionAttribute = NewString(LDAP_SETTINGS_DEFAULT_POSITION_ATTRIBUTE)
}
+ // For those upgrading to the version when LoginIdAttribute was added
+ // they need IdAttribute == LoginIdAttribute not to break
+ if s.LoginIdAttribute == nil {
+ s.LoginIdAttribute = s.IdAttribute
+ }
+
if s.SyncIntervalMinutes == nil {
s.SyncIntervalMinutes = NewInt(60)
}
@@ -2074,6 +2081,10 @@ func (ls *LdapSettings) isValid() *AppError {
if *ls.IdAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "", http.StatusBadRequest)
}
+
+ if *ls.LoginIdAttribute == "" {
+ return NewAppError("Config.IsValid", "model.config.is_valid.ldap_login_id", nil, "", http.StatusBadRequest)
+ }
}
return nil
diff --git a/model/switch_request.go b/model/switch_request.go
index e153c92f4..2a522f492 100644
--- a/model/switch_request.go
+++ b/model/switch_request.go
@@ -15,7 +15,7 @@ type SwitchRequest struct {
Password string `json:"password"`
NewPassword string `json:"new_password"`
MfaCode string `json:"mfa_code"`
- LdapId string `json:"ldap_id"`
+ LdapLoginId string `json:"ldap_id"`
}
func (o *SwitchRequest) ToJson() string {
diff --git a/model/websocket_client.go b/model/websocket_client.go
index 4ff4f617b..788dbee20 100644
--- a/model/websocket_client.go
+++ b/model/websocket_client.go
@@ -6,24 +6,28 @@ package model
import (
"encoding/json"
"net/http"
+ "time"
"github.com/gorilla/websocket"
)
const (
- SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB
+ SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB
+ PING_TIMEOUT_BUFFER_SECONDS = 5
)
type WebSocketClient struct {
- Url string // The location of the server like "ws://localhost:8065"
- ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
- ConnectUrl string // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket"
- Conn *websocket.Conn // The WebSocket connection
- AuthToken string // The token used to open the WebSocket
- Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
- EventChannel chan *WebSocketEvent
- ResponseChannel chan *WebSocketResponse
- ListenError *AppError
+ Url string // The location of the server like "ws://localhost:8065"
+ ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
+ ConnectUrl string // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket"
+ Conn *websocket.Conn // The WebSocket connection
+ AuthToken string // The token used to open the WebSocket
+ Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
+ PingTimeoutChannel chan bool // The channel used to signal ping timeouts
+ EventChannel chan *WebSocketEvent
+ ResponseChannel chan *WebSocketResponse
+ ListenError *AppError
+ pingTimeoutTimer *time.Timer
}
// NewWebSocketClient constructs a new WebSocket client with convenience
@@ -47,11 +51,15 @@ func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken strin
conn,
authToken,
1,
+ make(chan bool, 1),
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
nil,
+ nil,
}
+ client.configurePingHandling()
+
client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
return client, nil
@@ -78,11 +86,15 @@ func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken stri
conn,
authToken,
1,
+ make(chan bool, 1),
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
nil,
+ nil,
}
+ client.configurePingHandling()
+
client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
return client, nil
@@ -99,6 +111,8 @@ func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppErro
return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
}
+ wsc.configurePingHandling()
+
wsc.EventChannel = make(chan *WebSocketEvent, 100)
wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
@@ -181,3 +195,24 @@ func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
}
wsc.SendMessage("get_statuses_by_ids", data)
}
+
+func (wsc *WebSocketClient) configurePingHandling() {
+ wsc.Conn.SetPingHandler(wsc.pingHandler)
+ wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
+ go wsc.pingWatchdog()
+}
+
+func (wsc *WebSocketClient) pingHandler(appData string) error {
+ if !wsc.pingTimeoutTimer.Stop() {
+ <-wsc.pingTimeoutTimer.C
+ }
+
+ wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
+ wsc.Conn.WriteMessage(websocket.PongMessage, []byte{})
+ return nil
+}
+
+func (wsc *WebSocketClient) pingWatchdog() {
+ <-wsc.pingTimeoutTimer.C
+ wsc.PingTimeoutChannel <- true
+}
diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go
index 3bd87961a..eebc3ad69 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -270,25 +270,6 @@ func (s SqlChannelStore) Update(channel *model.Channel) store.StoreChannel {
})
}
-func (s SqlChannelStore) extraUpdated(channel *model.Channel) store.StoreChannel {
- return store.Do(func(result *store.StoreResult) {
- channel.ExtraUpdated()
-
- _, err := s.GetMaster().Exec(
- `UPDATE
- Channels
- SET
- ExtraUpdateAt = :Time
- WHERE
- Id = :Id`,
- map[string]interface{}{"Id": channel.Id, "Time": channel.ExtraUpdateAt})
-
- if err != nil {
- result.Err = model.NewAppError("SqlChannelStore.extraUpdated", "store.sql_channel.extra_updated.app_error", nil, "id="+channel.Id+", "+err.Error(), http.StatusInternalServerError)
- }
- })
-}
-
func (s SqlChannelStore) GetChannelUnread(channelId, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
var unreadChannel model.ChannelUnread
@@ -750,10 +731,6 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) store.StoreChan
if err := transaction.Commit(); err != nil {
result.Err = model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
- // If successfull record members have changed in channel
- if mu := <-s.extraUpdated(channel); mu.Err != nil {
- result.Err = mu.Err
- }
}
}
}
@@ -1055,21 +1032,9 @@ func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) s
func (s SqlChannelStore) RemoveMember(channelId string, userId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
- // Grab the channel we are saving this member to
- if cr := <-s.Get(channelId, true); cr.Err != nil {
- result.Err = cr.Err
- } else {
- channel := cr.Data.(*model.Channel)
-
- _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId})
- if err != nil {
- result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
- } else {
- // If successfull record members have changed in channel
- if mu := <-s.extraUpdated(channel); mu.Err != nil {
- result.Err = mu.Err
- }
- }
+ _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
}
})
}
diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go
index 562c36bf1..45515178d 100644
--- a/store/sqlstore/upgrade.go
+++ b/store/sqlstore/upgrade.go
@@ -15,6 +15,7 @@ import (
)
const (
+ VERSION_5_0_0 = "5.0.0"
VERSION_4_10_0 = "4.10.0"
VERSION_4_9_0 = "4.9.0"
VERSION_4_8_1 = "4.8.1"
@@ -76,6 +77,7 @@ func UpgradeDatabase(sqlStore SqlStore) {
UpgradeDatabaseToVersion481(sqlStore)
UpgradeDatabaseToVersion49(sqlStore)
UpgradeDatabaseToVersion410(sqlStore)
+ UpgradeDatabaseToVersion50(sqlStore)
// If the SchemaVersion is empty this this is the first time it has ran
// so lets set it to the current version.
@@ -421,3 +423,10 @@ func UpgradeDatabaseToVersion410(sqlStore SqlStore) {
sqlStore.GetMaster().Exec("UPDATE Users SET AuthData=LOWER(AuthData) WHERE AuthService = 'saml'")
}
}
+
+func UpgradeDatabaseToVersion50(sqlStore SqlStore) {
+ // TODO: Uncomment following condition when version 3.10.0 is released
+ //if shouldPerformUpgrade(sqlStore, VERSION_4_10_0, VERSION_5_0_0) {
+ // saveSchemaVersion(sqlStore, VERSION_5_0_0)
+ //}
+}
diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go
index f4ed3e400..a695e4aa8 100644
--- a/store/sqlstore/user_store.go
+++ b/store/sqlstore/user_store.go
@@ -819,13 +819,12 @@ func (us SqlUserStore) GetByUsername(username string) store.StoreChannel {
})
}
-func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) store.StoreChannel {
+func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail bool) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
params := map[string]interface{}{
"LoginId": loginId,
"AllowSignInWithUsername": allowSignInWithUsername,
"AllowSignInWithEmail": allowSignInWithEmail,
- "LdapEnabled": ldapEnabled,
}
users := []*model.User{}
@@ -837,8 +836,7 @@ func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allo
Users
WHERE
(:AllowSignInWithUsername AND Username = :LoginId)
- OR (:AllowSignInWithEmail AND Email = :LoginId)
- OR (:LdapEnabled AND AuthService = '`+model.USER_AUTH_SERVICE_LDAP+`' AND AuthData = :LoginId)`,
+ OR (:AllowSignInWithEmail AND Email = :LoginId)`,
params); err != nil {
result.Err = model.NewAppError("SqlUserStore.GetForLogin", "store.sql_user.get_for_login.app_error", nil, err.Error(), http.StatusInternalServerError)
} else if len(users) == 1 {
diff --git a/store/store.go b/store/store.go
index 0b5c9df5f..7fcebf0b1 100644
--- a/store/store.go
+++ b/store/store.go
@@ -228,7 +228,7 @@ type UserStore interface {
GetByAuth(authData *string, authService string) StoreChannel
GetAllUsingAuthService(authService string) StoreChannel
GetByUsername(username string) StoreChannel
- GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel
+ GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail bool) StoreChannel
VerifyEmail(userId string) StoreChannel
GetEtagForAllProfiles() StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go
index 481631783..ccb7b87b8 100644
--- a/store/storetest/channel_store.go
+++ b/store/storetest/channel_store.go
@@ -713,9 +713,6 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
c1.Type = model.CHANNEL_OPEN
c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel)
- c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t1 := c1t1.ExtraUpdateAt
-
u1 := model.User{}
u1.Email = model.NewId()
u1.Nickname = model.NewId()
@@ -740,13 +737,6 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&o2))
- c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t2 := c1t2.ExtraUpdateAt
-
- if t2 <= t1 {
- t.Fatal("Member update time incorrect")
- }
-
count := (<-ss.Channel().GetMemberCount(o1.ChannelId, true)).Data.(int64)
if count != 2 {
t.Fatal("should have saved 2 members")
@@ -777,13 +767,6 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
t.Fatal("should have removed 1 member")
}
- c1t3 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t3 := c1t3.ExtraUpdateAt
-
- if t3 <= t2 || t3 <= t1 {
- t.Fatal("Member update time incorrect on delete")
- }
-
member := (<-ss.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(*model.ChannelMember)
if member.ChannelId != o1.ChannelId {
t.Fatal("should have go member")
@@ -792,12 +775,6 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
if err := (<-ss.Channel().SaveMember(&o1)).Err; err == nil {
t.Fatal("Should have been a duplicate")
}
-
- c1t4 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t4 := c1t4.ExtraUpdateAt
- if t4 != t3 {
- t.Fatal("Should not update time upon failure")
- }
}
func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
@@ -808,9 +785,6 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
c1.Type = model.CHANNEL_OPEN
c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel)
- c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t1 := c1t1.ExtraUpdateAt
-
u1 := model.User{}
u1.Email = model.NewId()
u1.Nickname = model.NewId()
@@ -835,13 +809,6 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&o2))
- c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
- t2 := c1t2.ExtraUpdateAt
-
- if t2 <= t1 {
- t.Fatal("Member update time incorrect")
- }
-
count := (<-ss.Channel().GetMemberCount(o1.ChannelId, false)).Data.(int64)
if count != 2 {
t.Fatal("should have saved 2 members")
diff --git a/store/storetest/mocks/AuditStore.go b/store/storetest/mocks/AuditStore.go
index df84545bd..d1ee9082e 100644
--- a/store/storetest/mocks/AuditStore.go
+++ b/store/storetest/mocks/AuditStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/ChannelMemberHistoryStore.go b/store/storetest/mocks/ChannelMemberHistoryStore.go
index 16155b982..ae8d024d1 100644
--- a/store/storetest/mocks/ChannelMemberHistoryStore.go
+++ b/store/storetest/mocks/ChannelMemberHistoryStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go
index 6eab47073..ec3fa1253 100644
--- a/store/storetest/mocks/ChannelStore.go
+++ b/store/storetest/mocks/ChannelStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/ClusterDiscoveryStore.go b/store/storetest/mocks/ClusterDiscoveryStore.go
index 997dcf03f..4010006d8 100644
--- a/store/storetest/mocks/ClusterDiscoveryStore.go
+++ b/store/storetest/mocks/ClusterDiscoveryStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/CommandStore.go b/store/storetest/mocks/CommandStore.go
index de4bc4e34..798bbee4d 100644
--- a/store/storetest/mocks/CommandStore.go
+++ b/store/storetest/mocks/CommandStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/CommandWebhookStore.go b/store/storetest/mocks/CommandWebhookStore.go
index cede8cdd2..5129388ae 100644
--- a/store/storetest/mocks/CommandWebhookStore.go
+++ b/store/storetest/mocks/CommandWebhookStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/ComplianceStore.go b/store/storetest/mocks/ComplianceStore.go
index fb828cd4b..dd75941b3 100644
--- a/store/storetest/mocks/ComplianceStore.go
+++ b/store/storetest/mocks/ComplianceStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/EmojiStore.go b/store/storetest/mocks/EmojiStore.go
index 9871c98aa..b1f0a3217 100644
--- a/store/storetest/mocks/EmojiStore.go
+++ b/store/storetest/mocks/EmojiStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/FileInfoStore.go b/store/storetest/mocks/FileInfoStore.go
index 4dddf0bd7..67f922146 100644
--- a/store/storetest/mocks/FileInfoStore.go
+++ b/store/storetest/mocks/FileInfoStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/JobStore.go b/store/storetest/mocks/JobStore.go
index d3558212e..a78a3f94e 100644
--- a/store/storetest/mocks/JobStore.go
+++ b/store/storetest/mocks/JobStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go
index 6fa31bb1b..cf1d776ef 100644
--- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go
+++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go
index d4242708b..443112615 100644
--- a/store/storetest/mocks/LayeredStoreSupplier.go
+++ b/store/storetest/mocks/LayeredStoreSupplier.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/LicenseStore.go b/store/storetest/mocks/LicenseStore.go
index 5c65425ea..f00ebba78 100644
--- a/store/storetest/mocks/LicenseStore.go
+++ b/store/storetest/mocks/LicenseStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/OAuthStore.go b/store/storetest/mocks/OAuthStore.go
index fb49d715d..a39570b6c 100644
--- a/store/storetest/mocks/OAuthStore.go
+++ b/store/storetest/mocks/OAuthStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/PluginStore.go b/store/storetest/mocks/PluginStore.go
index 920b0f63c..b6f161a86 100644
--- a/store/storetest/mocks/PluginStore.go
+++ b/store/storetest/mocks/PluginStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go
index bdfbb3321..130bfafd7 100644
--- a/store/storetest/mocks/PostStore.go
+++ b/store/storetest/mocks/PostStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/PreferenceStore.go b/store/storetest/mocks/PreferenceStore.go
index 5ad56914a..f53ae06d5 100644
--- a/store/storetest/mocks/PreferenceStore.go
+++ b/store/storetest/mocks/PreferenceStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/ReactionStore.go b/store/storetest/mocks/ReactionStore.go
index ce09f3f76..b3e81a83b 100644
--- a/store/storetest/mocks/ReactionStore.go
+++ b/store/storetest/mocks/ReactionStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/RoleStore.go b/store/storetest/mocks/RoleStore.go
index 3c01ee341..cb69b2f2e 100644
--- a/store/storetest/mocks/RoleStore.go
+++ b/store/storetest/mocks/RoleStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/SessionStore.go b/store/storetest/mocks/SessionStore.go
index 70b2bd945..819ae948d 100644
--- a/store/storetest/mocks/SessionStore.go
+++ b/store/storetest/mocks/SessionStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go
index 43709fc0e..20cfd1721 100644
--- a/store/storetest/mocks/SqlStore.go
+++ b/store/storetest/mocks/SqlStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/StatusStore.go b/store/storetest/mocks/StatusStore.go
index 4acb90bdd..68ccdd4ec 100644
--- a/store/storetest/mocks/StatusStore.go
+++ b/store/storetest/mocks/StatusStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go
index cb7e511f6..a0438b66e 100644
--- a/store/storetest/mocks/Store.go
+++ b/store/storetest/mocks/Store.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/SystemStore.go b/store/storetest/mocks/SystemStore.go
index b31e4646d..e36396fe1 100644
--- a/store/storetest/mocks/SystemStore.go
+++ b/store/storetest/mocks/SystemStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go
index d38fb5f27..993c9b86f 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/TokenStore.go b/store/storetest/mocks/TokenStore.go
index b4baacf03..657aeca49 100644
--- a/store/storetest/mocks/TokenStore.go
+++ b/store/storetest/mocks/TokenStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/UserAccessTokenStore.go b/store/storetest/mocks/UserAccessTokenStore.go
index c5ef0fefe..fd98a8a99 100644
--- a/store/storetest/mocks/UserAccessTokenStore.go
+++ b/store/storetest/mocks/UserAccessTokenStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go
index 369a29e7a..347dd2065 100644
--- a/store/storetest/mocks/UserStore.go
+++ b/store/storetest/mocks/UserStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
@@ -258,13 +258,13 @@ func (_m *UserStore) GetEtagForProfilesNotInTeam(teamId string) store.StoreChann
return r0
}
-// GetForLogin provides a mock function with given fields: loginId, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled
-func (_m *UserStore) GetForLogin(loginId string, allowSignInWithUsername bool, allowSignInWithEmail bool, ldapEnabled bool) store.StoreChannel {
- ret := _m.Called(loginId, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled)
+// GetForLogin provides a mock function with given fields: loginId, allowSignInWithUsername, allowSignInWithEmail
+func (_m *UserStore) GetForLogin(loginId string, allowSignInWithUsername bool, allowSignInWithEmail bool) store.StoreChannel {
+ ret := _m.Called(loginId, allowSignInWithUsername, allowSignInWithEmail)
var r0 store.StoreChannel
- if rf, ok := ret.Get(0).(func(string, bool, bool, bool) store.StoreChannel); ok {
- r0 = rf(loginId, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled)
+ if rf, ok := ret.Get(0).(func(string, bool, bool) store.StoreChannel); ok {
+ r0 = rf(loginId, allowSignInWithUsername, allowSignInWithEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
diff --git a/store/storetest/mocks/WebhookStore.go b/store/storetest/mocks/WebhookStore.go
index bf5b636eb..a0b2b0bee 100644
--- a/store/storetest/mocks/WebhookStore.go
+++ b/store/storetest/mocks/WebhookStore.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v1.0.0
+// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go
index 2fd7d4190..66f54df39 100644
--- a/store/storetest/user_store.go
+++ b/store/storetest/user_store.go
@@ -1091,64 +1091,26 @@ func testUserStoreGetForLogin(t *testing.T, ss store.Store) {
}
store.Must(ss.User().Save(u2))
- if result := <-ss.User().GetForLogin(u1.Username, true, true, true); result.Err != nil {
+ if result := <-ss.User().GetForLogin(u1.Username, true, true); result.Err != nil {
t.Fatal("Should have gotten user by username", result.Err)
} else if result.Data.(*model.User).Id != u1.Id {
t.Fatal("Should have gotten user1 by username")
}
- if result := <-ss.User().GetForLogin(u1.Email, true, true, true); result.Err != nil {
+ if result := <-ss.User().GetForLogin(u1.Email, true, true); result.Err != nil {
t.Fatal("Should have gotten user by email", result.Err)
} else if result.Data.(*model.User).Id != u1.Id {
t.Fatal("Should have gotten user1 by email")
}
- if result := <-ss.User().GetForLogin(*u2.AuthData, true, true, true); result.Err != nil {
- t.Fatal("Should have gotten user by AD/LDAP AuthData", result.Err)
- } else if result.Data.(*model.User).Id != u2.Id {
- t.Fatal("Should have gotten user2 by AD/LDAP AuthData")
- }
-
- // prevent getting user by AuthData when they're not an LDAP user
- if result := <-ss.User().GetForLogin(*u1.AuthData, true, true, true); result.Err == nil {
- t.Fatal("Should not have gotten user by non-AD/LDAP AuthData")
- }
-
// prevent getting user when different login methods are disabled
- if result := <-ss.User().GetForLogin(u1.Username, false, true, true); result.Err == nil {
+ if result := <-ss.User().GetForLogin(u1.Username, false, true); result.Err == nil {
t.Fatal("Should have failed to get user1 by username")
}
- if result := <-ss.User().GetForLogin(u1.Email, true, false, true); result.Err == nil {
+ if result := <-ss.User().GetForLogin(u1.Email, true, false); result.Err == nil {
t.Fatal("Should have failed to get user1 by email")
}
-
- if result := <-ss.User().GetForLogin(*u2.AuthData, true, true, false); result.Err == nil {
- t.Fatal("Should have failed to get user3 by AD/LDAP AuthData")
- }
-
- auth3 := model.NewId()
-
- // test a special case where two users will have conflicting login information so we throw a special error
- u3 := &model.User{
- Email: model.NewId(),
- Username: model.NewId(),
- AuthService: model.USER_AUTH_SERVICE_LDAP,
- AuthData: &auth3,
- }
- store.Must(ss.User().Save(u3))
-
- u4 := &model.User{
- Email: model.NewId(),
- Username: model.NewId(),
- AuthService: model.USER_AUTH_SERVICE_LDAP,
- AuthData: &u3.Username,
- }
- store.Must(ss.User().Save(u4))
-
- if err := (<-ss.User().GetForLogin(u3.Username, true, true, true)).Err; err == nil {
- t.Fatal("Should have failed to get users with conflicting login information")
- }
}
func testUserStoreUpdatePassword(t *testing.T, ss store.Store) {
diff --git a/utils/file_backend.go b/utils/file_backend.go
index 42af7f604..0da0bc3b8 100644
--- a/utils/file_backend.go
+++ b/utils/file_backend.go
@@ -4,6 +4,7 @@
package utils
import (
+ "io"
"net/http"
"github.com/mattermost/mattermost-server/model"
@@ -15,7 +16,7 @@ type FileBackend interface {
ReadFile(path string) ([]byte, *model.AppError)
CopyFile(oldPath, newPath string) *model.AppError
MoveFile(oldPath, newPath string) *model.AppError
- WriteFile(f []byte, path string) *model.AppError
+ WriteFile(fr io.Reader, path string) (int64, *model.AppError)
RemoveFile(path string) *model.AppError
ListDirectory(path string) (*[]string, *model.AppError)
diff --git a/utils/file_backend_local.go b/utils/file_backend_local.go
index f85ace55a..37bca7987 100644
--- a/utils/file_backend_local.go
+++ b/utils/file_backend_local.go
@@ -4,6 +4,8 @@
package utils
import (
+ "bytes"
+ "io"
"io/ioutil"
"net/http"
"os"
@@ -22,8 +24,8 @@ type LocalFileBackend struct {
}
func (b *LocalFileBackend) TestConnection() *model.AppError {
- f := []byte("testingwrite")
- if err := writeFileLocally(f, filepath.Join(b.directory, TEST_FILE_PATH)); err != nil {
+ f := bytes.NewReader([]byte("testingwrite"))
+ if _, err := writeFileLocally(f, filepath.Join(b.directory, TEST_FILE_PATH)); err != nil {
return model.NewAppError("TestFileConnection", "Don't have permissions to write to local path specified or other error.", nil, err.Error(), http.StatusInternalServerError)
}
os.Remove(filepath.Join(b.directory, TEST_FILE_PATH))
@@ -58,21 +60,25 @@ func (b *LocalFileBackend) MoveFile(oldPath, newPath string) *model.AppError {
return nil
}
-func (b *LocalFileBackend) WriteFile(f []byte, path string) *model.AppError {
- return writeFileLocally(f, filepath.Join(b.directory, path))
+func (b *LocalFileBackend) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
+ return writeFileLocally(fr, filepath.Join(b.directory, path))
}
-func writeFileLocally(f []byte, path string) *model.AppError {
+func writeFileLocally(fr io.Reader, path string) (int64, *model.AppError) {
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
directory, _ := filepath.Abs(filepath.Dir(path))
- return model.NewAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error(), http.StatusInternalServerError)
+ return 0, model.NewAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error(), http.StatusInternalServerError)
}
-
- if err := ioutil.WriteFile(path, f, 0644); err != nil {
- return model.NewAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error(), http.StatusInternalServerError)
+ fw, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return 0, model.NewAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error(), http.StatusInternalServerError)
}
-
- return nil
+ defer fw.Close()
+ written, err := io.Copy(fw, fr)
+ if err != nil {
+ return written, model.NewAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ return written, nil
}
func (b *LocalFileBackend) RemoveFile(path string) *model.AppError {
diff --git a/utils/file_backend_s3.go b/utils/file_backend_s3.go
index 87ed3dd16..1772f09ea 100644
--- a/utils/file_backend_s3.go
+++ b/utils/file_backend_s3.go
@@ -4,7 +4,7 @@
package utils
import (
- "bytes"
+ "io"
"io/ioutil"
"net/http"
"os"
@@ -136,10 +136,10 @@ func (b *S3FileBackend) MoveFile(oldPath, newPath string) *model.AppError {
return nil
}
-func (b *S3FileBackend) WriteFile(f []byte, path string) *model.AppError {
+func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
s3Clnt, err := b.s3New()
if err != nil {
- return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
+ return 0, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var contentType string
@@ -150,12 +150,12 @@ func (b *S3FileBackend) WriteFile(f []byte, path string) *model.AppError {
}
options := s3PutOptions(b.encrypt, contentType)
-
- if _, err = s3Clnt.PutObject(b.bucket, path, bytes.NewReader(f), -1, options); err != nil {
- return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
+ written, err := s3Clnt.PutObject(b.bucket, path, fr, -1, options)
+ if err != nil {
+ return written, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
- return nil
+ return written, nil
}
func (b *S3FileBackend) RemoveFile(path string) *model.AppError {
diff --git a/utils/file_backend_test.go b/utils/file_backend_test.go
index 047e9df62..7f8265d73 100644
--- a/utils/file_backend_test.go
+++ b/utils/file_backend_test.go
@@ -4,6 +4,7 @@
package utils
import (
+ "bytes"
"fmt"
"io/ioutil"
"os"
@@ -95,7 +96,9 @@ func (s *FileBackendTestSuite) TestReadWriteFile() {
b := []byte("test")
path := "tests/" + model.NewId()
- s.Nil(s.backend.WriteFile(b, path))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path)
read, err := s.backend.ReadFile(path)
@@ -109,7 +112,9 @@ func (s *FileBackendTestSuite) TestReadWriteFileImage() {
b := []byte("testimage")
path := "tests/" + model.NewId() + ".png"
- s.Nil(s.backend.WriteFile(b, path))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path)
read, err := s.backend.ReadFile(path)
@@ -124,8 +129,9 @@ func (s *FileBackendTestSuite) TestCopyFile() {
path1 := "tests/" + model.NewId()
path2 := "tests/" + model.NewId()
- err := s.backend.WriteFile(b, path1)
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path1)
err = s.backend.CopyFile(path1, path2)
@@ -144,8 +150,9 @@ func (s *FileBackendTestSuite) TestCopyFileToDirectoryThatDoesntExist() {
path1 := "tests/" + model.NewId()
path2 := "tests/newdirectory/" + model.NewId()
- err := s.backend.WriteFile(b, path1)
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path1)
err = s.backend.CopyFile(path1, path2)
@@ -164,13 +171,15 @@ func (s *FileBackendTestSuite) TestMoveFile() {
path1 := "tests/" + model.NewId()
path2 := "tests/" + model.NewId()
- s.Nil(s.backend.WriteFile(b, path1))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path1)
s.Nil(s.backend.MoveFile(path1, path2))
defer s.backend.RemoveFile(path2)
- _, err := s.backend.ReadFile(path1)
+ _, err = s.backend.ReadFile(path1)
s.Error(err)
_, err = s.backend.ReadFile(path2)
@@ -181,15 +190,26 @@ func (s *FileBackendTestSuite) TestRemoveFile() {
b := []byte("test")
path := "tests/" + model.NewId()
- s.Nil(s.backend.WriteFile(b, path))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
s.Nil(s.backend.RemoveFile(path))
- _, err := s.backend.ReadFile(path)
+ _, err = s.backend.ReadFile(path)
s.Error(err)
- s.Nil(s.backend.WriteFile(b, "tests2/foo"))
- s.Nil(s.backend.WriteFile(b, "tests2/bar"))
- s.Nil(s.backend.WriteFile(b, "tests2/asdf"))
+ written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/foo")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
+
+ written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/bar")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
+
+ written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/asdf")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
+
s.Nil(s.backend.RemoveDirectory("tests2"))
}
@@ -198,9 +218,14 @@ func (s *FileBackendTestSuite) TestListDirectory() {
path1 := "19700101/" + model.NewId()
path2 := "19800101/" + model.NewId()
- s.Nil(s.backend.WriteFile(b, path1))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), path1)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path1)
- s.Nil(s.backend.WriteFile(b, path2))
+
+ written, err = s.backend.WriteFile(bytes.NewReader(b), path2)
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
defer s.backend.RemoveFile(path2)
paths, err := s.backend.ListDirectory("")
@@ -222,13 +247,21 @@ func (s *FileBackendTestSuite) TestListDirectory() {
func (s *FileBackendTestSuite) TestRemoveDirectory() {
b := []byte("test")
- s.Nil(s.backend.WriteFile(b, "tests2/foo"))
- s.Nil(s.backend.WriteFile(b, "tests2/bar"))
- s.Nil(s.backend.WriteFile(b, "tests2/aaa"))
+ written, err := s.backend.WriteFile(bytes.NewReader(b), "tests2/foo")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
+
+ written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/bar")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
+
+ written, err = s.backend.WriteFile(bytes.NewReader(b), "tests2/aaa")
+ s.Nil(err)
+ s.EqualValues(len(b), written, "expected given number of bytes to have been written")
s.Nil(s.backend.RemoveDirectory("tests2"))
- _, err := s.backend.ReadFile("tests2/foo")
+ _, err = s.backend.ReadFile("tests2/foo")
s.Error(err)
_, err = s.backend.ReadFile("tests2/bar")
s.Error(err)
diff --git a/utils/mail_test.go b/utils/mail_test.go
index 6bd8e7044..22a50df5f 100644
--- a/utils/mail_test.go
+++ b/utils/mail_test.go
@@ -150,8 +150,10 @@ func TestSendMailUsingConfig(t *testing.T) {
filePath2 := fmt.Sprintf("test2/%s", fileName)
fileContents1 := []byte("hello world")
fileContents2 := []byte("foo bar")
- assert.Nil(t, fileBackend.WriteFile(fileContents1, filePath1))
- assert.Nil(t, fileBackend.WriteFile(fileContents2, filePath2))
+ _, err := fileBackend.WriteFile(bytes.NewReader(fileContents1), filePath1)
+ assert.Nil(t, err)
+ _, err := fileBackend.WriteFile(bytes.NewReader(fileContents2), filePath2)
+ assert.Nil(t, err)
defer fileBackend.RemoveFile(filePath1)
defer fileBackend.RemoveFile(filePath2)
diff --git a/api4/context.go b/web/context.go
index dbe0c889f..711d241ae 100644
--- a/api4/context.go
+++ b/web/context.go
@@ -1,14 +1,13 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api4
+package web
import (
"fmt"
"net/http"
"regexp"
"strings"
- "time"
goi18n "github.com/nicksnyder/go-i18n/i18n"
@@ -21,7 +20,7 @@ import (
type Context struct {
App *app.App
Session model.Session
- Params *ApiParams
+ Params *Params
Err *model.AppError
T goi18n.TranslateFunc
RequestId string
@@ -30,169 +29,6 @@ type Context struct {
siteURLHeader string
}
-func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{
- app: api.App,
- handleFunc: h,
- requireSession: false,
- trustRequester: false,
- requireMfa: false,
- }
-}
-
-func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{
- app: api.App,
- handleFunc: h,
- requireSession: true,
- trustRequester: false,
- requireMfa: true,
- }
-}
-
-func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{
- app: api.App,
- handleFunc: h,
- requireSession: true,
- trustRequester: false,
- requireMfa: false,
- }
-}
-
-func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{
- app: api.App,
- handleFunc: h,
- requireSession: false,
- trustRequester: true,
- requireMfa: false,
- }
-}
-
-func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
- return &handler{
- app: api.App,
- handleFunc: h,
- requireSession: true,
- trustRequester: true,
- requireMfa: true,
- }
-}
-
-type handler struct {
- app *app.App
- handleFunc func(*Context, http.ResponseWriter, *http.Request)
- requireSession bool
- trustRequester bool
- requireMfa bool
-}
-
-func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- now := time.Now()
- mlog.Debug(fmt.Sprintf("%v - %v", r.Method, r.URL.Path))
-
- c := &Context{}
- c.App = h.app
- c.T, _ = utils.GetTranslationsAndLocale(w, r)
- c.RequestId = model.NewId()
- c.IpAddress = utils.GetIpAddress(r)
- c.Params = ApiParamsFromRequest(r)
-
- token, tokenLocation := app.ParseAuthTokenFromRequest(r)
-
- // CSRF Check
- if tokenLocation == app.TokenLocationCookie && h.requireSession && !h.trustRequester {
- if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
- c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
- token = ""
- }
- }
-
- c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host)
-
- w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
- w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))
-
- w.Header().Set("Content-Type", "application/json")
-
- if r.Method == "GET" {
- w.Header().Set("Expires", "0")
- }
-
- if len(token) != 0 {
- session, err := c.App.GetSession(token)
-
- if err != nil {
- mlog.Info(fmt.Sprintf("Invalid session err=%v", err.Error()))
- if err.StatusCode == http.StatusInternalServerError {
- c.Err = err
- } else if h.requireSession {
- c.RemoveSessionCookie(w, r)
- c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
- }
- } else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
- c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
- } else {
- c.Session = *session
- }
-
- // Rate limit by UserID
- if c.App.Srv.RateLimiter != nil && c.App.Srv.RateLimiter.UserIdRateLimit(c.Session.UserId, w) {
- return
- }
- }
-
- c.Path = r.URL.Path
-
- if c.Err == nil && h.requireSession {
- c.SessionRequired()
- }
-
- if c.Err == nil && h.requireMfa {
- c.MfaRequired()
- }
-
- if c.Err == nil {
- h.handleFunc(c, w, r)
- }
-
- // Handle errors that have occurred
- if c.Err != nil {
- c.Err.Translate(c.T)
- c.Err.RequestId = c.RequestId
-
- if c.Err.Id == "api.context.session_expired.app_error" {
- c.LogInfo(c.Err)
- } else {
- c.LogError(c.Err)
- }
-
- c.Err.Where = r.URL.Path
-
- // Block out detailed error when not in developer mode
- if !*c.App.Config().ServiceSettings.EnableDeveloper {
- c.Err.DetailedError = ""
- }
-
- w.WriteHeader(c.Err.StatusCode)
- w.Write([]byte(c.Err.ToJson()))
-
- if c.App.Metrics != nil {
- c.App.Metrics.IncrementHttpError()
- }
- }
-
- if c.App.Metrics != nil {
- c.App.Metrics.IncrementHttpRequest()
-
- if r.URL.Path != model.API_URL_SUFFIX+"/websocket" {
- elapsed := float64(time.Since(now)) / float64(time.Second)
- c.App.Metrics.ObserveHttpRequestDuration(elapsed)
- }
- }
-}
-
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-c.App.Srv.Store.Audit().Save(audit); r.Err != nil {
diff --git a/api4/context_test.go b/web/context_test.go
index 302b7b24b..3fa6ebf22 100644
--- a/api4/context_test.go
+++ b/web/context_test.go
@@ -1,4 +1,4 @@
-package api4
+package web
import (
"net/http"
@@ -8,7 +8,7 @@ import (
func TestRequireHookId(t *testing.T) {
c := &Context{}
t.Run("WhenHookIdIsValid", func(t *testing.T) {
- c.Params = &ApiParams{HookId: "abcdefghijklmnopqrstuvwxyz"}
+ c.Params = &Params{HookId: "abcdefghijklmnopqrstuvwxyz"}
c.RequireHookId()
if c.Err != nil {
@@ -17,7 +17,7 @@ func TestRequireHookId(t *testing.T) {
})
t.Run("WhenHookIdIsInvalid", func(t *testing.T) {
- c.Params = &ApiParams{HookId: "abc"}
+ c.Params = &Params{HookId: "abc"}
c.RequireHookId()
if c.Err == nil {
diff --git a/web/handlers.go b/web/handlers.go
new file mode 100644
index 000000000..e2521674a
--- /dev/null
+++ b/web/handlers.go
@@ -0,0 +1,158 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/utils"
+)
+
+func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &Handler{
+ App: w.App,
+ HandleFunc: h,
+ RequireSession: false,
+ TrustRequester: false,
+ RequireMfa: false,
+ IsStatic: false,
+ }
+}
+
+func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &Handler{
+ App: w.App,
+ HandleFunc: h,
+ RequireSession: false,
+ TrustRequester: false,
+ RequireMfa: false,
+ IsStatic: true,
+ }
+}
+
+type Handler struct {
+ App *app.App
+ HandleFunc func(*Context, http.ResponseWriter, *http.Request)
+ RequireSession bool
+ TrustRequester bool
+ RequireMfa bool
+ IsStatic bool
+}
+
+func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ now := time.Now()
+ mlog.Debug(fmt.Sprintf("%v - %v", r.Method, r.URL.Path))
+
+ c := &Context{}
+ c.App = h.App
+ c.T, _ = utils.GetTranslationsAndLocale(w, r)
+ c.RequestId = model.NewId()
+ c.IpAddress = utils.GetIpAddress(r)
+ c.Params = ParamsFromRequest(r)
+
+ token, tokenLocation := app.ParseAuthTokenFromRequest(r)
+
+ // CSRF Check
+ if tokenLocation == app.TokenLocationCookie && h.RequireSession && !h.TrustRequester {
+ if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
+ c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
+ token = ""
+ }
+ }
+
+ c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host)
+
+ w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
+ w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))
+
+ if h.IsStatic {
+ // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
+ w.Header().Set("X-Frame-Options", "SAMEORIGIN")
+ w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
+ } else {
+ // All api response bodies will be JSON formatted by default
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.Method == "GET" {
+ w.Header().Set("Expires", "0")
+ }
+ }
+
+ if len(token) != 0 {
+ session, err := c.App.GetSession(token)
+
+ if err != nil {
+ mlog.Info(fmt.Sprintf("Invalid session err=%v", err.Error()))
+ if err.StatusCode == http.StatusInternalServerError {
+ c.Err = err
+ } else if h.RequireSession {
+ c.RemoveSessionCookie(w, r)
+ c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
+ }
+ } else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
+ c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
+ } else {
+ c.Session = *session
+ }
+
+ // Rate limit by UserID
+ if c.App.Srv.RateLimiter != nil && c.App.Srv.RateLimiter.UserIdRateLimit(c.Session.UserId, w) {
+ return
+ }
+ }
+
+ c.Path = r.URL.Path
+
+ if c.Err == nil && h.RequireSession {
+ c.SessionRequired()
+ }
+
+ if c.Err == nil && h.RequireMfa {
+ c.MfaRequired()
+ }
+
+ if c.Err == nil {
+ h.HandleFunc(c, w, r)
+ }
+
+ // Handle errors that have occurred
+ if c.Err != nil {
+ c.Err.Translate(c.T)
+ c.Err.RequestId = c.RequestId
+
+ if c.Err.Id == "api.context.session_expired.app_error" {
+ c.LogInfo(c.Err)
+ } else {
+ c.LogError(c.Err)
+ }
+
+ c.Err.Where = r.URL.Path
+
+ // Block out detailed error when not in developer mode
+ if !*c.App.Config().ServiceSettings.EnableDeveloper {
+ c.Err.DetailedError = ""
+ }
+
+ w.WriteHeader(c.Err.StatusCode)
+ w.Write([]byte(c.Err.ToJson()))
+
+ if c.App.Metrics != nil {
+ c.App.Metrics.IncrementHttpError()
+ }
+ }
+
+ if c.App.Metrics != nil {
+ c.App.Metrics.IncrementHttpRequest()
+
+ if r.URL.Path != model.API_URL_SUFFIX+"/websocket" {
+ elapsed := float64(time.Since(now)) / float64(time.Second)
+ c.App.Metrics.ObserveHttpRequestDuration(elapsed)
+ }
+ }
+}
diff --git a/api4/params.go b/web/params.go
index e8e3f25e7..286c3f44f 100644
--- a/api4/params.go
+++ b/web/params.go
@@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api4
+package web
import (
"net/http"
@@ -19,7 +19,7 @@ const (
LOGS_PER_PAGE_MAXIMUM = 10000
)
-type ApiParams struct {
+type Params struct {
UserId string
TeamId string
InviteId string
@@ -53,8 +53,8 @@ type ApiParams struct {
Permanent bool
}
-func ApiParamsFromRequest(r *http.Request) *ApiParams {
- params := &ApiParams{}
+func ParamsFromRequest(r *http.Request) *Params {
+ params := &Params{}
props := mux.Vars(r)
query := r.URL.Query()
diff --git a/web/saml.go b/web/saml.go
new file mode 100644
index 000000000..f3e5a12e8
--- /dev/null
+++ b/web/saml.go
@@ -0,0 +1,149 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ b64 "encoding/base64"
+ "net/http"
+ "strings"
+
+ "github.com/mattermost/mattermost-server/app"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (w *Web) InitSaml() {
+ w.MainRouter.Handle("/login/sso/saml", w.NewHandler(loginWithSaml)).Methods("GET")
+ w.MainRouter.Handle("/login/sso/saml", w.NewHandler(completeSaml)).Methods("POST")
+}
+
+func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
+ samlInterface := c.App.Saml
+
+ if samlInterface == nil {
+ c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
+ return
+ }
+
+ teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
+ if err != nil {
+ c.Err = err
+ return
+ }
+ action := r.URL.Query().Get("action")
+ redirectTo := r.URL.Query().Get("redirect_to")
+ relayProps := map[string]string{}
+ relayState := ""
+
+ if len(action) != 0 {
+ relayProps["team_id"] = teamId
+ relayProps["action"] = action
+ if action == model.OAUTH_ACTION_EMAIL_TO_SSO {
+ relayProps["email"] = r.URL.Query().Get("email")
+ }
+ }
+
+ if len(redirectTo) != 0 {
+ relayProps["redirect_to"] = redirectTo
+ }
+
+ if len(relayProps) > 0 {
+ relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJson(relayProps)))
+ }
+
+ if data, err := samlInterface.BuildRequest(relayState); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
+ http.Redirect(w, r, data.URL, http.StatusFound)
+ }
+}
+
+func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
+ samlInterface := c.App.Saml
+
+ if samlInterface == nil {
+ c.Err = model.NewAppError("completeSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
+ return
+ }
+
+ //Validate that the user is with SAML and all that
+ encodedXML := r.FormValue("SAMLResponse")
+ relayState := r.FormValue("RelayState")
+
+ relayProps := make(map[string]string)
+ if len(relayState) > 0 {
+ stateStr := ""
+ if b, err := b64.StdEncoding.DecodeString(relayState); err != nil {
+ c.Err = model.NewAppError("completeSaml", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error(), http.StatusFound)
+ return
+ } else {
+ stateStr = string(b)
+ }
+ relayProps = model.MapFromJson(strings.NewReader(stateStr))
+ }
+
+ action := relayProps["action"]
+ if user, err := samlInterface.DoLogin(encodedXML, relayProps); err != nil {
+ if action == model.OAUTH_ACTION_MOBILE {
+ err.Translate(c.T)
+ w.Write([]byte(err.ToJson()))
+ } else {
+ c.Err = err
+ c.Err.StatusCode = http.StatusFound
+ }
+ return
+ } else {
+ if err := c.App.CheckUserAllAuthenticationCriteria(user, ""); err != nil {
+ c.Err = err
+ c.Err.StatusCode = http.StatusFound
+ return
+ }
+
+ switch action {
+ case model.OAUTH_ACTION_SIGNUP:
+ teamId := relayProps["team_id"]
+ if len(teamId) > 0 {
+ c.App.Go(func() {
+ if err := c.App.AddUserToTeamByTeamId(teamId, user); err != nil {
+ mlog.Error(err.Error())
+ } else {
+ c.App.AddDirectChannels(teamId, user)
+ }
+ })
+ }
+ case model.OAUTH_ACTION_EMAIL_TO_SSO:
+ if err := c.App.RevokeAllSessions(user.Id); err != nil {
+ c.Err = err
+ return
+ }
+ c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
+ c.App.Go(func() {
+ if err := c.App.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
+ mlog.Error(err.Error())
+ }
+ })
+ }
+
+ session, err := c.App.DoLogin(w, r, user, "")
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ c.Session = *session
+
+ if val, ok := relayProps["redirect_to"]; ok {
+ http.Redirect(w, r, c.GetSiteURLHeader()+val, http.StatusFound)
+ return
+ }
+
+ if action == model.OAUTH_ACTION_MOBILE {
+ ReturnStatusOK(w)
+ } else {
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusFound)
+ }
+ }
+}
diff --git a/web/static.go b/web/static.go
new file mode 100644
index 000000000..487526fdf
--- /dev/null
+++ b/web/static.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/NYTimes/gziphandler"
+
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/utils"
+)
+
+func (w *Web) InitStatic() {
+ if *w.App.Config().ServiceSettings.WebserverMode != "disabled" {
+ staticDir, _ := utils.FindDir(model.CLIENT_DIR)
+ mlog.Debug(fmt.Sprintf("Using client directory at %v", staticDir))
+
+ staticHandler := staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
+ pluginHandler := pluginHandler(w.App.Config, http.StripPrefix("/static/plugins/", http.FileServer(http.Dir(*w.App.Config().PluginSettings.ClientDirectory))))
+
+ if *w.App.Config().ServiceSettings.WebserverMode == "gzip" {
+ staticHandler = gziphandler.GzipHandler(staticHandler)
+ pluginHandler = gziphandler.GzipHandler(pluginHandler)
+ }
+
+ w.MainRouter.PathPrefix("/static/plugins/").Handler(pluginHandler)
+ w.MainRouter.PathPrefix("/static/").Handler(staticHandler)
+ w.MainRouter.Handle("/{anything:.*}", w.NewStaticHandler(root)).Methods("GET")
+ }
+}
+
+func root(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ if !CheckClientCompatability(r.UserAgent()) {
+ w.Header().Set("Cache-Control", "no-store")
+ page := utils.NewHTMLTemplate(c.App.HTMLTemplates(), "unsupported_browser")
+ page.Props["Title"] = c.T("web.error.unsupported_browser.title")
+ page.Props["Message"] = c.T("web.error.unsupported_browser.message")
+ page.RenderToWriter(w)
+ return
+ }
+
+ if IsApiCall(r) {
+ Handle404(c.App, w, r)
+ return
+ }
+
+ w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
+
+ staticDir, _ := utils.FindDir(model.CLIENT_DIR)
+ http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
+}
+
+func staticHandler(handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "max-age=31556926, public")
+ if strings.HasSuffix(r.URL.Path, "/") {
+ http.NotFound(w, r)
+ return
+ }
+ handler.ServeHTTP(w, r)
+ })
+}
+
+func pluginHandler(config model.ConfigFunc, handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if *config().ServiceSettings.EnableDeveloper {
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ } else {
+ w.Header().Set("Cache-Control", "max-age=31556926, public")
+ }
+ if strings.HasSuffix(r.URL.Path, "/") {
+ http.NotFound(w, r)
+ return
+ }
+ handler.ServeHTTP(w, r)
+ })
+}
diff --git a/web/web.go b/web/web.go
index 56a5ab6ac..53276953e 100644
--- a/web/web.go
+++ b/web/web.go
@@ -6,69 +6,39 @@ package web
import (
"fmt"
"net/http"
- "path/filepath"
"strings"
- "github.com/NYTimes/gziphandler"
"github.com/avct/uasurfer"
+ "github.com/gorilla/mux"
- "github.com/mattermost/mattermost-server/api"
+ "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
)
-func Init(api3 *api.API) {
- mlog.Debug("Initializing web routes")
-
- mainrouter := api3.BaseRoutes.Root
-
- if *api3.App.Config().ServiceSettings.WebserverMode != "disabled" {
- staticDir, _ := utils.FindDir(model.CLIENT_DIR)
- mlog.Debug(fmt.Sprintf("Using client directory at %v", staticDir))
-
- staticHandler := staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
- pluginHandler := pluginHandler(api3.App.Config, http.StripPrefix("/static/plugins/", http.FileServer(http.Dir(*api3.App.Config().PluginSettings.ClientDirectory))))
+type Web struct {
+ App *app.App
+ MainRouter *mux.Router
+}
- if *api3.App.Config().ServiceSettings.WebserverMode == "gzip" {
- staticHandler = gziphandler.GzipHandler(staticHandler)
- pluginHandler = gziphandler.GzipHandler(pluginHandler)
- }
+func NewWeb(a *app.App, root *mux.Router) *Web {
+ mlog.Debug("Initializing web routes")
- mainrouter.PathPrefix("/static/plugins/").Handler(pluginHandler)
- mainrouter.PathPrefix("/static/").Handler(staticHandler)
- mainrouter.Handle("/{anything:.*}", api3.AppHandlerIndependent(root)).Methods("GET")
+ web := &Web{
+ App: a,
+ MainRouter: root,
}
-}
-func staticHandler(handler http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Cache-Control", "max-age=31556926, public")
- if strings.HasSuffix(r.URL.Path, "/") {
- http.NotFound(w, r)
- return
- }
- handler.ServeHTTP(w, r)
- })
-}
+ web.InitStatic()
+ web.InitWebhooks()
+ web.InitSaml()
-func pluginHandler(config model.ConfigFunc, handler http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if *config().ServiceSettings.EnableDeveloper {
- w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
- } else {
- w.Header().Set("Cache-Control", "max-age=31556926, public")
- }
- if strings.HasSuffix(r.URL.Path, "/") {
- http.NotFound(w, r)
- return
- }
- handler.ServeHTTP(w, r)
- })
+ return web
}
-// Due to the complexities of UA detection and the ramifications of a misdetection only older Safari and IE browsers throw incompatibility errors.
-
+// Due to the complexities of UA detection and the ramifications of a misdetection
+// only older Safari and IE browsers throw incompatibility errors.
// Map should be of minimum required browser version.
var browserMinimumSupported = map[string]int{
"BrowserIE": 11,
@@ -85,24 +55,26 @@ func CheckClientCompatability(agentString string) bool {
return true
}
-func root(c *api.Context, 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)
- if !CheckClientCompatability(r.UserAgent()) {
- w.Header().Set("Cache-Control", "no-store")
- page := utils.NewHTMLTemplate(c.App.HTMLTemplates(), "unsupported_browser")
- page.Props["Title"] = c.T("web.error.unsupported_browser.title")
- page.Props["Message"] = c.T("web.error.unsupported_browser.message")
- page.RenderToWriter(w)
- return
- }
+ mlog.Debug(fmt.Sprintf("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)))
- if api.IsApiCall(r) {
- api.Handle404(c.App, w, r)
- return
+ if IsApiCall(r) {
+ w.WriteHeader(err.StatusCode)
+ 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.RenderWebAppError(w, r, err, a.AsymmetricSigningKey())
}
+}
- w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
+func IsApiCall(r *http.Request) bool {
+ return strings.Index(r.URL.Path, "/api/") == 0
+}
- staticDir, _ := utils.FindDir(model.CLIENT_DIR)
- http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
+func ReturnStatusOK(w http.ResponseWriter) {
+ m := make(map[string]string)
+ m[model.STATUS] = model.STATUS_OK
+ w.Write([]byte(model.MapToJson(m)))
}
diff --git a/web/web_test.go b/web/web_test.go
index 12099709e..4497f00cc 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -8,8 +8,6 @@ import (
"os"
"testing"
- "github.com/mattermost/mattermost-server/api"
- "github.com/mattermost/mattermost-server/api4"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
@@ -38,7 +36,14 @@ func StopTestStore() {
}
}
-func Setup() *app.App {
+type TestHelper struct {
+ App *app.App
+ BasicUser *model.User
+ BasicChannel *model.Channel
+ BasicTeam *model.Team
+}
+
+func Setup() *TestHelper {
a, err := app.New(app.StoreOverride(testStore), app.DisableConfigWatch)
if err != nil {
panic(err)
@@ -50,9 +55,8 @@ func Setup() *app.App {
panic(serverErr)
}
a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
- api4.Init(a, a.Srv.Router, false)
- api3 := api.Init(a, a.Srv.Router)
- Init(api3)
+
+ NewWeb(a, a.Srv.Router)
URL = fmt.Sprintf("http://localhost:%v", a.Srv.ListenAddr.Port)
ApiClient = model.NewClient(URL)
@@ -65,11 +69,31 @@ func Setup() *app.App {
*cfg.ServiceSettings.EnableAPIv3 = true
})
- return a
+ th := &TestHelper{
+ App: a,
+ }
+
+ return th
+}
+
+func (th *TestHelper) InitBasic() *TestHelper {
+ user, _ := th.App.CreateUser(&model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SYSTEM_ADMIN_ROLE_ID})
+
+ team, _ := th.App.CreateTeam(&model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: user.Email, Type: model.TEAM_OPEN})
+
+ th.App.JoinUserToTeam(team, user, "")
+
+ channel, _ := th.App.CreateChannel(&model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id, CreatorId: user.Id}, true)
+
+ th.BasicUser = user
+ th.BasicChannel = channel
+ th.BasicTeam = team
+
+ return th
}
-func TearDown(a *app.App) {
- a.Shutdown()
+func (th *TestHelper) TearDown() {
+ th.App.Shutdown()
if err := recover(); err != nil {
StopTestStore()
panic(err)
@@ -93,63 +117,6 @@ func TestStatic(t *testing.T) {
}
*/
-func TestIncomingWebhook(t *testing.T) {
- a := Setup()
- defer TearDown(a)
-
- user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
- user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User)
- store.Must(a.Srv.Store.User().VerifyEmail(user.Id))
-
- ApiClient.Login(user.Email, "passwd1")
-
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team)
-
- a.JoinUserToTeam(team, user, "")
-
- a.UpdateUserRoles(user.Id, model.SYSTEM_ADMIN_ROLE_ID, false)
- ApiClient.SetTeamId(team.Id)
-
- channel1 := &model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel1 = ApiClient.Must(ApiClient.CreateChannel(channel1)).Data.(*model.Channel)
-
- if a.Config().ServiceSettings.EnableIncomingWebhooks {
- hook1 := &model.IncomingWebhook{ChannelId: channel1.Id}
- hook1 = ApiClient.Must(ApiClient.CreateIncomingWebhook(hook1)).Data.(*model.IncomingWebhook)
-
- payload := "payload={\"text\": \"test text\"}"
- if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err != nil {
- t.Fatal(err)
- }
-
- payload = "payload={\"text\": \"\"}"
- if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err == nil {
- t.Fatal("should have errored - no text to post")
- }
-
- payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}"
- if _, err := ApiClient.PostToWebhook(hook1.Id, payload); err == nil {
- t.Fatal("should have errored - bad channel")
- }
-
- payload = "payload={\"text\": \"test text\"}"
- if _, err := ApiClient.PostToWebhook("abc123", payload); err == nil {
- t.Fatal("should have errored - bad hook")
- }
-
- payloadMultiPart := "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nwebhook-bot\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nthis is a test :tada:\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--"
- if _, err := ApiClient.DoPost("/hooks/"+hook1.Id, payloadMultiPart, "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"); err != nil {
- t.Fatal("should have errored - bad hook")
- }
-
- } else {
- if _, err := ApiClient.PostToWebhook("123", "123"); err == nil {
- t.Fatal("should have failed - webhooks turned off")
- }
- }
-}
-
func TestMain(m *testing.M) {
// Setup a global logger to catch tests logging outside of app context
// The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
diff --git a/web/webhook.go b/web/webhook.go
new file mode 100644
index 000000000..55cdeb9b5
--- /dev/null
+++ b/web/webhook.go
@@ -0,0 +1,101 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/mux"
+ "github.com/gorilla/schema"
+
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (w *Web) InitWebhooks() {
+ w.MainRouter.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", w.NewHandler(commandWebhook)).Methods("POST")
+ w.MainRouter.Handle("/hooks/{id:[A-Za-z0-9]+}", w.NewHandler(incomingWebhook)).Methods("POST")
+}
+
+func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id := params["id"]
+
+ r.ParseForm()
+
+ var err *model.AppError
+ incomingWebhookPayload := &model.IncomingWebhookRequest{}
+ contentType := r.Header.Get("Content-Type")
+ if strings.Split(contentType, "; ")[0] == "application/x-www-form-urlencoded" {
+ payload := strings.NewReader(r.FormValue("payload"))
+
+ incomingWebhookPayload, err = decodePayload(payload)
+ if err != nil {
+ c.Err = err
+ return
+ }
+ } else if strings.HasPrefix(contentType, "multipart/form-data") {
+ r.ParseMultipartForm(0)
+
+ decoder := schema.NewDecoder()
+ err := decoder.Decode(incomingWebhookPayload, r.PostForm)
+
+ if err != nil {
+ c.Err = model.NewAppError("incomingWebhook", "api.webhook.incoming.error", nil, err.Error(), http.StatusBadRequest)
+ return
+ }
+ } else {
+ incomingWebhookPayload, err = decodePayload(r.Body)
+ if err != nil {
+ c.Err = err
+ return
+ }
+ }
+
+ if c.App.Config().LogSettings.EnableWebhookDebugging {
+ mlog.Debug(fmt.Sprint("Incoming webhook received. Content=", incomingWebhookPayload.ToJson()))
+ }
+
+ err = c.App.HandleIncomingWebhook(id, incomingWebhookPayload)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte("ok"))
+}
+
+func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id := params["id"]
+
+ response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
+ if err != nil {
+ c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ appErr := c.App.HandleCommandWebhook(id, response)
+ if appErr != nil {
+ c.Err = appErr
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte("ok"))
+}
+
+func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) {
+ incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJson(payload)
+
+ if decodeError != nil {
+ return nil, decodeError
+ }
+
+ return incomingWebhookPayload, nil
+}
diff --git a/web/webhook_test.go b/web/webhook_test.go
new file mode 100644
index 000000000..e625e55bb
--- /dev/null
+++ b/web/webhook_test.go
@@ -0,0 +1,216 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package web
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func TestIncomingWebhook(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if !th.App.Config().ServiceSettings.EnableIncomingWebhooks {
+ _, err := ApiClient.PostToWebhook("123", "123")
+ assert.NotNil(t, err, "should have errored - webhooks turned off")
+ return
+ }
+
+ hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
+ require.Nil(t, err)
+
+ url := "/hooks/" + hook.Id
+
+ tooLongText := ""
+ for i := 0; i < 8200; i++ {
+ tooLongText += "a"
+ }
+
+ t.Run("WebhookBasics", func(t *testing.T) {
+ payload := "payload={\"text\": \"test text\"}"
+ _, err := ApiClient.DoPost(url, payload, "application/x-www-form-urlencoded")
+ assert.Nil(t, err)
+
+ payload = "payload={\"text\": \"\"}"
+ _, err = ApiClient.DoPost(url, payload, "application/x-www-form-urlencoded")
+ assert.NotNil(t, err, "should have errored - no text to post")
+
+ payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}"
+ _, err = ApiClient.DoPost(url, payload, "application/x-www-form-urlencoded")
+ assert.NotNil(t, err, "should have errored - bad channel")
+
+ payload = "payload={\"text\": \"test text\"}"
+ _, err = ApiClient.DoPost("/hooks/abc123", payload, "application/x-www-form-urlencoded")
+ assert.NotNil(t, err, "should have errored - bad hook")
+
+ _, err = ApiClient.DoPost(url, "{\"text\":\"this is a test\"}", "application/json")
+ assert.Nil(t, err)
+
+ text := `this is a \"test\"
+ that contains a newline and a tab`
+ _, err = ApiClient.DoPost(url, "{\"text\":\""+text+"\"}", "application/json")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name), "application/json")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", th.BasicChannel.Name), "application/json")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", th.BasicUser.Username), "application/json")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, "payload={\"text\":\"this is a test\"}", "application/x-www-form-urlencoded")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, "payload={\"text\":\""+text+"\"}", "application/x-www-form-urlencoded")
+ assert.Nil(t, err)
+
+ _, err = ApiClient.DoPost(url, "{\"text\":\""+tooLongText+"\"}", "application/json")
+ assert.Nil(t, err)
+
+ payloadMultiPart := "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nwebhook-bot\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nthis is a test :tada:\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--"
+ _, err = ApiClient.DoPost("/hooks/"+hook.Id, payloadMultiPart, "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW")
+ assert.Nil(t, err)
+ })
+
+ t.Run("WebhookExperimentReadOnly", func(t *testing.T) {
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false })
+ _, err := ApiClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL), "application/json")
+ assert.Nil(t, err, "Not read only")
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
+ th.App.SetLicense(model.NewTestLicense())
+ })
+
+ t.Run("WebhookAttachments", func(t *testing.T) {
+ attachmentPayload := `{
+ "text": "this is a test",
+ "attachments": [
+ {
+ "fallback": "Required plain-text summary of the attachment.",
+
+ "color": "#36a64f",
+
+ "pretext": "Optional text that appears above the attachment block",
+
+ "author_name": "Bobby Tables",
+ "author_link": "http://flickr.com/bobby/",
+ "author_icon": "http://flickr.com/icons/bobby.jpg",
+
+ "title": "Slack API Documentation",
+ "title_link": "https://api.slack.com/",
+
+ "text": "Optional text that appears within the attachment",
+
+ "fields": [
+ {
+ "title": "Priority",
+ "value": "High",
+ "short": false
+ }
+ ],
+
+ "image_url": "http://my-website.com/path/to/image.jpg",
+ "thumb_url": "http://example.com/path/to/thumb.png"
+ }
+ ]
+ }`
+
+ _, err := ApiClient.DoPost(url, attachmentPayload, "application/json")
+ assert.Nil(t, err)
+
+ attachmentPayload = `{
+ "text": "this is a test",
+ "attachments": [
+ {
+ "fallback": "Required plain-text summary of the attachment.",
+
+ "color": "#36a64f",
+
+ "pretext": "Optional text that appears above the attachment block",
+
+ "author_name": "Bobby Tables",
+ "author_link": "http://flickr.com/bobby/",
+ "author_icon": "http://flickr.com/icons/bobby.jpg",
+
+ "title": "Slack API Documentation",
+ "title_link": "https://api.slack.com/",
+
+ "text": "` + tooLongText + `",
+
+ "fields": [
+ {
+ "title": "Priority",
+ "value": "High",
+ "short": false
+ }
+ ],
+
+ "image_url": "http://my-website.com/path/to/image.jpg",
+ "thumb_url": "http://example.com/path/to/thumb.png"
+ }
+ ]
+ }`
+
+ _, err = ApiClient.DoPost(url, attachmentPayload, "application/json")
+ assert.Nil(t, err)
+ })
+
+ t.Run("DisableWebhooks", func(t *testing.T) {
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = false })
+ _, err := ApiClient.DoPost(url, "{\"text\":\"this is a test\"}", "application/json")
+ assert.NotNil(t, err)
+ })
+}
+
+func TestCommandWebhooks(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ cmd, err := th.App.CreateCommand(&model.Command{
+ CreatorId: th.BasicUser.Id,
+ TeamId: th.BasicTeam.Id,
+ URL: "http://nowhere.com",
+ Method: model.COMMAND_METHOD_POST,
+ Trigger: "delayed"})
+ require.Nil(t, err)
+
+ args := &model.CommandArgs{
+ TeamId: th.BasicTeam.Id,
+ UserId: th.BasicUser.Id,
+ ChannelId: th.BasicChannel.Id,
+ }
+
+ hook, err := th.App.CreateCommandWebhook(cmd.Id, args)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound {
+ t.Fatal("expected not-found for non-existent hook")
+ }
+
+ if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest {
+ t.Fatal(err)
+ }
+
+ for i := 0; i < 5; i++ {
+ if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK {
+ t.Fatal(err)
+ }
+ }
+
+ if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest {
+ t.Fatal("expected error for sixth usage")
+ }
+}