summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-01-30 08:30:02 -0500
committerGitHub <noreply@github.com>2017-01-30 08:30:02 -0500
commitc01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e (patch)
treef995a08e296b5088df2a882ab70251c7b2b8cfe7
parent3e2f879b77b9b9d089bc8f83304b8b21b83c5bd9 (diff)
downloadchat-c01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e.tar.gz
chat-c01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e.tar.bz2
chat-c01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e.zip
Implement APIv4 infrastructure (#5191)
* Implement APIv4 infrastructure * Update parameter requirement functions per feedback
-rw-r--r--Makefile4
-rw-r--r--api/api.go2
-rw-r--r--api/command_test.go4
-rw-r--r--api/context.go41
-rw-r--r--api/file_test.go3
-rw-r--r--api/oauth.go10
-rw-r--r--api/user.go229
-rw-r--r--api/user_test.go2
-rw-r--r--api4/api.go189
-rw-r--r--api4/apitestlib.go211
-rw-r--r--api4/context.go360
-rw-r--r--api4/params.go61
-rw-r--r--api4/user.go169
-rw-r--r--api4/user_test.go186
-rw-r--r--app/authentication.go (renamed from api/authentication.go)21
-rw-r--r--app/file.go6
-rw-r--r--app/login.go136
-rw-r--r--app/user.go108
-rw-r--r--cmd/platform/server.go2
-rw-r--r--glide.lock2
-rw-r--r--glide.yaml2
-rw-r--r--i18n/en.json10
-rw-r--r--model/client.go5
-rw-r--r--model/client4.go222
-rw-r--r--model/user.go25
-rw-r--r--model/utils.go12
-rw-r--r--model/websocket_client.go4
-rw-r--r--store/sql_user_store.go6
-rw-r--r--utils/password.go6
-rw-r--r--vendor/github.com/gorilla/mux/mux_test.go58
-rw-r--r--vendor/github.com/gorilla/mux/route.go2
31 files changed, 1826 insertions, 272 deletions
diff --git a/Makefile b/Makefile
index 980a9b89d..e47237795 100644
--- a/Makefile
+++ b/Makefile
@@ -203,6 +203,7 @@ test-server: start-docker prepare-enterprise
echo "mode: count" > cover.out
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi.out ./api || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi4.out ./api4 || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=capp.out ./app || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
@@ -210,12 +211,13 @@ test-server: start-docker prepare-enterprise
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cweb.out ./web || exit 1
tail -n +2 capi.out >> cover.out
+ tail -n +2 capi4.out >> cover.out
tail -n +2 capp.out >> cover.out
tail -n +2 cmodel.out >> cover.out
tail -n +2 cstore.out >> cover.out
tail -n +2 cutils.out >> cover.out
tail -n +2 cweb.out >> cover.out
- rm -f capi.out capp.out cmodel.out cstore.out cutils.out cweb.out
+ rm -f capi.out capi4.out capp.out cmodel.out cstore.out cutils.out cweb.out
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Running Enterprise tests
diff --git a/api/api.go b/api/api.go
index 59c547b8c..8f7e6c37e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -67,7 +67,7 @@ func InitRouter() {
func InitApi() {
BaseRoutes = &Routes{}
BaseRoutes.Root = app.Srv.Router
- BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
+ BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX_V3).Subrouter()
BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter()
BaseRoutes.NeedUser = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter()
diff --git a/api/command_test.go b/api/command_test.go
index 726a9cb9b..8194a4c60 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -239,7 +239,7 @@ func TestTestCommand(t *testing.T) {
*utils.Cfg.ServiceSettings.EnableCommands = true
cmd1 := &model.Command{
- URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test",
+ URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX_V3 + "/teams/command_test",
Method: model.COMMAND_METHOD_POST,
Trigger: "test",
}
@@ -259,7 +259,7 @@ func TestTestCommand(t *testing.T) {
}
cmd2 := &model.Command{
- URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test",
+ URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX_V3 + "/teams/command_test",
Method: model.COMMAND_METHOD_GET,
Trigger: "test2",
}
diff --git a/api/context.go b/api/context.go
index e998138a6..21989f775 100644
--- a/api/context.go
+++ b/api/context.go
@@ -21,17 +21,18 @@ import (
)
type Context struct {
- Session model.Session
- RequestId string
- IpAddress string
- Path string
- Err *model.AppError
- siteURL string
- teamURLValid bool
- teamURL string
- T goi18n.TranslateFunc
- Locale string
- TeamId string
+ Session model.Session
+ RequestId string
+ IpAddress string
+ Path string
+ Err *model.AppError
+ siteURL string
+ teamURLValid bool
+ teamURL string
+ T goi18n.TranslateFunc
+ Locale string
+ TeamId string
+ isSystemAdmin bool
}
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
@@ -142,7 +143,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if utils.GetSiteURL() == "" {
- protocol := GetProtocol(r)
+ protocol := app.GetProtocol(r)
c.SetSiteURL(protocol + "://" + r.Host)
} else {
c.SetSiteURL(utils.GetSiteURL())
@@ -251,21 +252,13 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.isApi && einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementHttpRequest()
- if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" {
+ if r.URL.Path != model.API_URL_SUFFIX_V3+"/users/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
einterfaces.GetMetricsInterface().ObserveHttpRequestDuration(elapsed)
}
}
}
-func GetProtocol(r *http.Request) string {
- if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
- return "https"
- } else {
- return "http"
- }
-}
-
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 := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
@@ -347,13 +340,17 @@ func (c *Context) SystemAdminRequired() {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
- } else if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ } else if !c.IsSystemAdmin() {
c.Err = model.NewLocAppError("", "api.context.permissions.app_error", nil, "AdminRequired")
c.Err.StatusCode = http.StatusForbidden
return
}
}
+func (c *Context) IsSystemAdmin() bool {
+ return app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
+}
+
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
diff --git a/api/file_test.go b/api/file_test.go
index ce3e1fab4..c004bb562 100644
--- a/api/file_test.go
+++ b/api/file_test.go
@@ -406,6 +406,7 @@ func TestGetPublicFile(t *testing.T) {
time.Sleep(2 * time.Second)
if resp, err := http.Get(link); err != nil || resp.StatusCode != http.StatusOK {
+ t.Log(link)
t.Fatal("failed to get image with public link", err)
}
@@ -509,7 +510,7 @@ func TestGetPublicFileOld(t *testing.T) {
func generatePublicLinkOld(siteURL, teamId, channelId, userId, filename string) string {
hash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
- return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash)
+ return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX_V3, teamId, channelId, userId, filename, hash)
}
func TestGetPublicLink(t *testing.T) {
diff --git a/api/oauth.go b/api/oauth.go
index abb216414..659d5c129 100644
--- a/api/oauth.go
+++ b/api/oauth.go
@@ -291,7 +291,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
doLogin(c, w, r, user, "")
}
if c.Err == nil {
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_LOGIN:
@@ -304,25 +304,25 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusTemporaryRedirect)
return
}
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
CompleteSwitchWithOAuth(c, w, r, service, body, props["email"])
if c.Err == nil {
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_SSO_TO_EMAIL:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
}
break
default:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
}
diff --git a/api/user.go b/api/user.go
index 7722e917b..bfe2db14e 100644
--- a/api/user.go
+++ b/api/user.go
@@ -20,7 +20,6 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
- "github.com/mssola/user_agent"
)
func InitUser() {
@@ -77,12 +76,6 @@ func InitUser() {
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation {
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_email_disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
user := model.UserFromJson(r.Body)
if user == nil {
@@ -90,45 +83,25 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- user.EmailVerified = false
-
hash := r.URL.Query().Get("h")
inviteId := r.URL.Query().Get("iid")
var ruser *model.User
var err *model.AppError
if len(hash) > 0 {
- data := r.URL.Query().Get("d")
- ruser, err = app.CreateUserWithHash(user, hash, data)
- if err != nil {
- c.Err = err
- return
- }
+ ruser, err = app.CreateUserWithHash(user, hash, r.URL.Query().Get("d"))
} else if len(inviteId) > 0 {
ruser, err = app.CreateUserWithInviteId(user, inviteId, c.GetSiteURL())
- if err != nil {
- c.Err = err
- return
- }
} else {
- if !app.IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email)
- return
- }
-
- ruser, err = app.CreateUser(user)
- if err != nil {
- c.Err = err
- return
- }
+ ruser, err = app.CreateUserFromSignup(user, c.GetSiteURL())
+ }
- if err := app.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, c.GetSiteURL()); err != nil {
- l4g.Error(err.Error())
- }
+ if err != nil {
+ c.Err = err
+ return
}
w.Write([]byte(ruser.ToJson()))
-
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -141,56 +114,15 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
- if len(password) == 0 {
- c.Err = model.NewLocAppError("login", "api.user.login.blank_pwd.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- var user *model.User
- var err *model.AppError
-
- if len(id) != 0 {
- c.LogAuditWithUserId(id, "attempt")
-
- if user, err = app.GetUser(id); err != nil {
- c.LogAuditWithUserId(id, "failure")
- c.Err = err
- c.Err.StatusCode = http.StatusBadRequest
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementLoginFail()
- }
- return
- }
- } else {
- c.LogAudit("attempt")
-
- if user, err = app.GetUserForLogin(loginId, ldapOnly); err != nil {
- c.LogAudit("failure")
- c.Err = err
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementLoginFail()
- }
- return
- }
-
- c.LogAuditWithUserId(user.Id, "attempt")
- }
-
- // and then authenticate them
- if user, err = authenticateUser(user, password, mfaToken); err != nil {
- c.LogAuditWithUserId(user.Id, "failure")
+ c.LogAudit("attempt - user_id=" + id + " login_id=" + loginId)
+ user, err := app.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
+ if err != nil {
+ c.LogAudit("failure - user_id=" + id + " login_id=" + loginId)
c.Err = err
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementLoginFail()
- }
return
}
c.LogAuditWithUserId(user.Id, "success")
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementLogin()
- }
doLogin(c, w, r, user, deviceId)
if c.Err != nil {
@@ -244,77 +176,12 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
// User MUST be authenticated completely before calling Login
func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
-
- session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false}
-
- maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24
-
- if len(deviceId) > 0 {
- session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
- maxAge = *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
-
- // A special case where we logout of all other sessions with the same Id
- if err := app.RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
- c.Err = err
- c.Err.StatusCode = http.StatusInternalServerError
- return
- }
- } else {
- session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
- }
-
- ua := user_agent.New(r.UserAgent())
-
- plat := ua.Platform()
- if plat == "" {
- plat = "unknown"
- }
-
- os := ua.OS()
- if os == "" {
- os = "unknown"
- }
-
- bname, bversion := ua.Browser()
- if bname == "" {
- bname = "unknown"
- }
-
- if bversion == "" {
- bversion = "0.0"
- }
-
- session.AddProp(model.SESSION_PROP_PLATFORM, plat)
- session.AddProp(model.SESSION_PROP_OS, os)
- session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
-
- var err *model.AppError
- if session, err = app.CreateSession(session); err != nil {
+ session, err := app.DoLogin(w, r, user, deviceId)
+ if err != nil {
c.Err = err
- c.Err.StatusCode = http.StatusInternalServerError
return
}
- w.Header().Set(model.HEADER_TOKEN, session.Token)
-
- secure := false
- if GetProtocol(r) == "https" {
- secure = true
- }
-
- expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
- sessionCookie := &http.Cookie{
- Name: model.SESSION_COOKIE_TOKEN,
- Value: session.Token,
- Path: "/",
- MaxAge: maxAge,
- Expires: expiresAt,
- HttpOnly: true,
- Secure: secure,
- }
-
- http.SetCookie(w, sessionCookie)
-
c.Session = *session
}
@@ -357,7 +224,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
maxAge := *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
secure := false
- if GetProtocol(r) == "https" {
+ if app.GetProtocol(r) == "https" {
secure = true
}
@@ -502,12 +369,15 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
if user, err = app.GetUser(id); err != nil {
c.Err = err
return
- } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) {
+ }
+
+ etag := user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)
+
+ if HandleEtag(etag, "Get User", w, r) {
return
} else {
- sanitizeProfile(c, user)
-
- w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
+ app.SanitizeProfile(user, c.IsSystemAdmin())
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(user.ToJson()))
return
}
@@ -829,24 +699,11 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if ruser, err := app.UpdateUser(user, c.GetSiteURL()); err != nil {
+ if ruser, err := app.UpdateUserAsUser(user, c.GetSiteURL(), c.IsSystemAdmin()); err != nil {
c.Err = err
return
} else {
c.LogAudit("")
-
- updatedUser := ruser
- updatedUser = sanitizeProfile(c, updatedUser)
-
- omitUsers := make(map[string]bool, 1)
- omitUsers[user.Id] = true
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
- message.Add("user", updatedUser)
- go app.Publish(message)
-
- ruser.Password = ""
- ruser.AuthData = new(string)
- *ruser.AuthData = ""
w.Write([]byte(ruser.ToJson()))
}
}
@@ -875,38 +732,8 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var user *model.User
- var err *model.AppError
-
- if user, err = app.GetUser(userId); err != nil {
- c.Err = err
- return
- }
-
- if user == nil {
- c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if user.AuthData != nil && *user.AuthData != "" {
- c.LogAudit("failed - tried to update user password who was logged in through oauth")
- c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if err := doubleCheckPassword(user, currentPassword); err != nil {
- if err.Id == "api.user.check_user_password.invalid.app_error" {
- c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "")
- } else {
- c.Err = err
- }
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- if err := app.UpdatePasswordSendEmail(user, newPassword, c.T("api.user.update_password.menu"), c.GetSiteURL()); err != nil {
+ if err := app.UpdatePasswordAsUser(userId, currentPassword, newPassword, c.GetSiteURL()); err != nil {
+ c.LogAudit("failed")
c.Err = err
return
} else {
@@ -1110,7 +937,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
+ if err := app.CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err
return
@@ -1238,7 +1065,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := checkPasswordAndAllCriteria(user, emailPassword, token); err != nil {
+ if err := app.CheckPasswordAndAllCriteria(user, emailPassword, token); err != nil {
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err
return
@@ -1332,7 +1159,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := checkUserMfa(user, token); err != nil {
+ if err := app.CheckUserMfa(user, token); err != nil {
c.LogAuditWithUserId(user.Id, "fail - mfa token failed")
c.Err = err
return
@@ -1600,7 +1427,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err.StatusCode = http.StatusFound
return
} else {
- if err := checkUserAdditionalAuthenticationCriteria(user, ""); err != nil {
+ if err := app.CheckUserAdditionalAuthenticationCriteria(user, ""); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusFound
return
@@ -1635,7 +1462,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusFound)
return
}
- http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusFound)
+ http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusFound)
}
}
diff --git a/api/user_test.go b/api/user_test.go
index 5a398a716..bf1059d2a 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -634,7 +634,7 @@ func TestGetAudits(t *testing.T) {
t.Fatal(err)
} else {
- if len(result.Data.(model.Audits)) != 2 {
+ if len(result.Data.(model.Audits)) != 1 {
t.Fatal(result.Data.(model.Audits))
}
diff --git a/api4/api.go b/api4/api.go
new file mode 100644
index 000000000..9314bb616
--- /dev/null
+++ b/api4/api.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+
+ _ "github.com/nicksnyder/go-i18n/i18n"
+)
+
+type Routes struct {
+ Root *mux.Router // ''
+ ApiRoot *mux.Router // 'api/v4'
+
+ Users *mux.Router // 'api/v4/users'
+ User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
+ UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9_-\.]+}'
+ UserByEmail *mux.Router // 'api/v4/users/email/{email}'
+
+ Teams *mux.Router // 'api/v4/teams'
+ TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
+ Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
+ TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
+ TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9_-]+}/members'
+ TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9_-]+}/members/{user_id:[A-Za-z0-9_-]+}'
+
+ Channels *mux.Router // 'api/v4/channels'
+ Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
+ ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
+ ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
+ ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
+ ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
+ ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/members'
+
+ Posts *mux.Router // 'api/v4/posts'
+ Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
+ PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
+
+ Files *mux.Router // 'api/v4/files'
+ File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
+
+ Commands *mux.Router // 'api/v4/commands'
+ Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
+ CommandsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/commands'
+
+ Hooks *mux.Router // 'api/v4/teams/hooks'
+ IncomingHooks *mux.Router // 'api/v4/teams/hooks/incoming'
+ IncomingHook *mux.Router // 'api/v4/teams/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
+ OutgoingHooks *mux.Router // 'api/v4/teams/hooks/outgoing'
+ OutgoingHook *mux.Router // 'api/v4/teams/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
+
+ OAuth *mux.Router // 'api/v4/oauth'
+
+ Admin *mux.Router // 'api/v4/admin'
+
+ System *mux.Router // 'api/v4/system'
+
+ Preferences *mux.Router // 'api/v4/preferences'
+
+ License *mux.Router // 'api/v4/license'
+
+ Public *mux.Router // 'api/v4/public'
+
+ Emojis *mux.Router // 'api/v4/emoji'
+ Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
+
+ Webrtc *mux.Router // 'api/v4/webrtc'
+}
+
+var BaseRoutes *Routes
+
+func InitRouter() {
+ app.Srv.Router = mux.NewRouter()
+ app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404)
+ app.Srv.WebSocketRouter = app.NewWebSocketRouter()
+}
+
+func InitApi(full bool) {
+ BaseRoutes = &Routes{}
+ BaseRoutes.Root = app.Srv.Router
+ BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
+
+ BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter()
+ BaseRoutes.User = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.UserByUsername = BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9_-.]+}").Subrouter()
+ BaseRoutes.UserByEmail = BaseRoutes.Users.PathPrefix("/email/{email}").Subrouter()
+
+ BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter()
+ BaseRoutes.TeamsForUser = BaseRoutes.Users.PathPrefix("/teams").Subrouter()
+ BaseRoutes.Team = BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.TeamByName = BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
+ BaseRoutes.TeamMembers = BaseRoutes.Team.PathPrefix("/members").Subrouter()
+ BaseRoutes.TeamMember = BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
+
+ BaseRoutes.Channels = BaseRoutes.ApiRoot.PathPrefix("/channels").Subrouter()
+ BaseRoutes.Channel = BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.ChannelByName = BaseRoutes.Team.PathPrefix("/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
+ BaseRoutes.ChannelsForTeam = BaseRoutes.Team.PathPrefix("/channels").Subrouter()
+ BaseRoutes.ChannelMembers = BaseRoutes.Channel.PathPrefix("/members").Subrouter()
+ BaseRoutes.ChannelMember = BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.ChannelMembersForUser = BaseRoutes.User.PathPrefix("/channels/members").Subrouter()
+
+ BaseRoutes.Posts = BaseRoutes.ApiRoot.PathPrefix("/posts").Subrouter()
+ BaseRoutes.Post = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.PostsForChannel = BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
+
+ BaseRoutes.Files = BaseRoutes.ApiRoot.PathPrefix("/files").Subrouter()
+ BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
+
+ BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter()
+ BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.CommandsForTeam = BaseRoutes.Team.PathPrefix("/commands").Subrouter()
+
+ BaseRoutes.Hooks = BaseRoutes.ApiRoot.PathPrefix("/hooks").Subrouter()
+ BaseRoutes.IncomingHooks = BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
+ BaseRoutes.IncomingHook = BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
+ BaseRoutes.OutgoingHooks = BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
+ BaseRoutes.OutgoingHook = BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
+
+ BaseRoutes.OAuth = BaseRoutes.ApiRoot.PathPrefix("/oauth").Subrouter()
+ BaseRoutes.Admin = BaseRoutes.ApiRoot.PathPrefix("/admin").Subrouter()
+ BaseRoutes.System = BaseRoutes.ApiRoot.PathPrefix("/system").Subrouter()
+ BaseRoutes.Preferences = BaseRoutes.ApiRoot.PathPrefix("/preferences").Subrouter()
+ BaseRoutes.License = BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter()
+ BaseRoutes.Public = BaseRoutes.ApiRoot.PathPrefix("/public").Subrouter()
+
+ BaseRoutes.Emojis = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter()
+ BaseRoutes.Emoji = BaseRoutes.Emojis.PathPrefix("/{emoji_id:[A-Za-z0-9]+}").Subrouter()
+
+ BaseRoutes.Webrtc = BaseRoutes.ApiRoot.PathPrefix("/webrtc").Subrouter()
+
+ InitUser()
+
+ // REMOVE CONDITION WHEN APIv3 REMOVED
+ if full {
+ // 404 on any api route before web.go has a chance to serve it
+ app.Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
+
+ utils.InitHTML()
+
+ app.InitEmailBatching()
+ }
+}
+
+func HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
+ metrics := einterfaces.GetMetricsInterface()
+ if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 {
+ if et == etag {
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.WriteHeader(http.StatusNotModified)
+ if metrics != nil {
+ metrics.IncrementEtagHitCounter(routeName)
+ }
+ return true
+ }
+ }
+
+ if metrics != nil {
+ metrics.IncrementEtagMissCounter(routeName)
+ }
+
+ return false
+}
+
+func Handle404(w http.ResponseWriter, r *http.Request) {
+ err := model.NewLocAppError("Handle404", "api.context.404.app_error", nil, "")
+ err.Translate(utils.T)
+ err.StatusCode = http.StatusNotFound
+
+ l4g.Debug("%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 ReturnStatusOK(w http.ResponseWriter) {
+ m := make(map[string]string)
+ m[model.STATUS] = model.STATUS_OK
+ w.Write([]byte(model.MapToJson(m)))
+}
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
new file mode 100644
index 000000000..05e8d76fe
--- /dev/null
+++ b/api4/apitestlib.go
@@ -0,0 +1,211 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+ "reflect"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+type TestHelper struct {
+ Client *model.Client4
+ BasicUser *model.User
+ SystemAdminUser *model.User
+}
+
+func Setup() *TestHelper {
+ if app.Srv == nil {
+ utils.TranslationsPreInit()
+ utils.LoadConfig("config.json")
+ utils.InitTranslations(utils.Cfg.LocalizationSettings)
+ utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
+ *utils.Cfg.RateLimitSettings.Enable = false
+ utils.Cfg.EmailSettings.SendEmailNotifications = true
+ utils.Cfg.EmailSettings.SMTPServer = "dockerhost"
+ utils.Cfg.EmailSettings.SMTPPort = "2500"
+ utils.Cfg.EmailSettings.FeedbackEmail = "test@example.com"
+ utils.DisableDebugLogForTest()
+ app.NewServer()
+ app.InitStores()
+ InitRouter()
+ app.StartServer()
+ InitApi(true)
+ utils.EnableDebugLogForTest()
+ app.Srv.Store.MarkSystemRanUnitTests()
+
+ *utils.Cfg.TeamSettings.EnableOpenServer = true
+ }
+
+ th := &TestHelper{}
+ th.Client = th.CreateClient()
+ return th
+}
+
+func (me *TestHelper) InitBasic() *TestHelper {
+ me.BasicUser = me.CreateUser()
+ app.UpdateUserRoles(me.BasicUser.Id, model.ROLE_SYSTEM_USER.Id)
+ me.LoginBasic()
+
+ return me
+}
+
+func (me *TestHelper) InitSystemAdmin() *TestHelper {
+ me.SystemAdminUser = me.CreateUser()
+ app.UpdateUserRoles(me.SystemAdminUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_ADMIN.Id)
+
+ return me
+}
+
+func (me *TestHelper) CreateClient() *model.Client4 {
+ return model.NewAPIv4Client("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
+}
+
+func (me *TestHelper) CreateUser() *model.User {
+ id := model.NewId()
+
+ user := &model.User{
+ Email: GenerateTestEmail(),
+ Username: GenerateTestUsername(),
+ Nickname: "nn_" + id,
+ FirstName: "f_" + id,
+ LastName: "l_" + id,
+ Password: "Password1",
+ }
+
+ utils.DisableDebugLogForTest()
+ ruser, _ := me.Client.CreateUser(user)
+ ruser.Password = "Password1"
+ VerifyUserEmail(ruser.Id)
+ utils.EnableDebugLogForTest()
+ return ruser
+}
+
+func (me *TestHelper) LoginBasic() {
+ utils.DisableDebugLogForTest()
+ me.Client.Login(me.BasicUser.Email, me.BasicUser.Password)
+ utils.EnableDebugLogForTest()
+}
+
+func (me *TestHelper) LoginSystemAdmin() {
+ utils.DisableDebugLogForTest()
+ me.Client.Login(me.SystemAdminUser.Email, me.SystemAdminUser.Password)
+ utils.EnableDebugLogForTest()
+}
+
+func GenerateTestEmail() string {
+ return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
+}
+
+func GenerateTestUsername() string {
+ return "n" + model.NewId()
+}
+
+func VerifyUserEmail(userId string) {
+ store.Must(app.Srv.Store.User().VerifyEmail(userId))
+}
+
+func CheckUserSanitization(t *testing.T, user *model.User) {
+ if user.Password != "" {
+ t.Fatal("password wasn't blank")
+ }
+
+ if user.AuthData != nil && *user.AuthData != "" {
+ t.Fatal("auth data wasn't blank")
+ }
+
+ if user.MfaSecret != "" {
+ t.Fatal("mfa secret wasn't blank")
+ }
+}
+
+func CheckEtag(t *testing.T, data interface{}, resp *model.Response) {
+ if !reflect.ValueOf(data).IsNil() {
+ t.Fatal("etag data was not nil")
+ }
+
+ if resp.StatusCode != http.StatusNotModified {
+ t.Log("actual: " + strconv.Itoa(resp.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusNotModified))
+ t.Fatal("wrong status code for etag")
+ }
+}
+
+func CheckNoError(t *testing.T, resp *model.Response) {
+ if resp.Error != nil {
+ t.Fatal(resp.Error)
+ }
+}
+
+func CheckForbiddenStatus(t *testing.T, resp *model.Response) {
+ if resp.Error == nil {
+ t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusForbidden))
+ return
+ }
+
+ if resp.StatusCode != http.StatusForbidden {
+ t.Log("actual: " + strconv.Itoa(resp.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusForbidden))
+ t.Fatal("wrong status code")
+ }
+}
+
+func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) {
+ if resp.Error == nil {
+ t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusUnauthorized))
+ return
+ }
+
+ if resp.StatusCode != http.StatusUnauthorized {
+ t.Log("actual: " + strconv.Itoa(resp.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusUnauthorized))
+ t.Fatal("wrong status code")
+ }
+}
+
+func CheckNotFoundStatus(t *testing.T, resp *model.Response) {
+ if resp.Error == nil {
+ t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusNotFound))
+ return
+ }
+
+ if resp.StatusCode != http.StatusNotFound {
+ t.Log("actual: " + strconv.Itoa(resp.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusNotFound))
+ t.Fatal("wrong status code")
+ }
+}
+
+func CheckBadRequestStatus(t *testing.T, resp *model.Response) {
+ if resp.Error == nil {
+ t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusBadRequest))
+ return
+ }
+
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Log("actual: " + strconv.Itoa(resp.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
+ t.Fatal("wrong status code")
+ }
+}
+
+func CheckErrorMessage(t *testing.T, resp *model.Response, message string) {
+ if resp.Error == nil {
+ t.Fatal("should have errored with message:" + message)
+ return
+ }
+
+ if resp.Error.Message != message {
+ t.Log("actual: " + resp.Error.Message)
+ t.Log("expected: " + message)
+ t.Fatal("incorrect error message")
+ }
+}
diff --git a/api4/context.go b/api4/context.go
new file mode 100644
index 000000000..1a3011795
--- /dev/null
+++ b/api4/context.go
@@ -0,0 +1,360 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ l4g "github.com/alecthomas/log4go"
+ goi18n "github.com/nicksnyder/go-i18n/i18n"
+
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+type Context struct {
+ Session model.Session
+ Params *ApiParams
+ Err *model.AppError
+ T goi18n.TranslateFunc
+ RequestId string
+ IpAddress string
+ Path string
+ siteURL string
+}
+
+func ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &handler{
+ handleFunc: h,
+ requireSession: false,
+ trustRequester: false,
+ requireMfa: false,
+ }
+}
+
+func ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &handler{
+ handleFunc: h,
+ requireSession: true,
+ trustRequester: false,
+ requireMfa: true,
+ }
+}
+
+func ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &handler{
+ handleFunc: h,
+ requireSession: true,
+ trustRequester: false,
+ requireMfa: false,
+ }
+}
+
+func ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &handler{
+ handleFunc: h,
+ requireSession: false,
+ trustRequester: true,
+ requireMfa: false,
+ }
+}
+
+func ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
+ return &handler{
+ handleFunc: h,
+ requireSession: true,
+ trustRequester: true,
+ requireMfa: true,
+ }
+}
+
+type handler struct {
+ 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()
+ l4g.Debug("%v - %v", r.Method, r.URL.Path)
+
+ c := &Context{}
+ c.T, _ = utils.GetTranslationsAndLocale(w, r)
+ c.RequestId = model.NewId()
+ c.IpAddress = utils.GetIpAddress(r)
+ c.Params = ApiParamsFromRequest(r)
+
+ token := ""
+ isTokenFromQueryString := false
+
+ // Attempt to parse token out of the header
+ authHeader := r.Header.Get(model.HEADER_AUTH)
+ if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
+ // Default session token
+ token = authHeader[7:]
+
+ } else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
+ // OAuth token
+ token = authHeader[6:]
+ }
+
+ // Attempt to parse the token from the cookie
+ if len(token) == 0 {
+ if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
+ token = cookie.Value
+
+ if h.requireSession && !h.trustRequester {
+ if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
+ c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt")
+ token = ""
+ }
+ }
+ }
+ }
+
+ // Attempt to parse token out of the query string
+ if len(token) == 0 {
+ token = r.URL.Query().Get("access_token")
+ isTokenFromQueryString = true
+ }
+
+ if utils.GetSiteURL() == "" {
+ protocol := app.GetProtocol(r)
+ c.SetSiteURL(protocol + "://" + r.Host)
+ }
+
+ w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
+ w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash))
+ if einterfaces.GetClusterInterface() != nil {
+ w.Header().Set(model.HEADER_CLUSTER_ID, einterfaces.GetClusterInterface().GetClusterId())
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.Method == "GET" {
+ w.Header().Set("Expires", "0")
+ }
+
+ if len(token) != 0 {
+ session, err := app.GetSession(token)
+
+ if err != nil {
+ l4g.Error(utils.T("api.context.invalid_session.error"), err.Error())
+ c.RemoveSessionCookie(w, r)
+ if h.requireSession {
+ c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
+ c.Err.StatusCode = http.StatusUnauthorized
+ }
+ } else if !session.IsOAuth && isTokenFromQueryString {
+ c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
+ c.Err.StatusCode = http.StatusUnauthorized
+ } else {
+ c.Session = *session
+ }
+ }
+
+ 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 occured
+ if c.Err != nil {
+ c.Err.Translate(c.T)
+ c.Err.RequestId = c.RequestId
+ c.LogError(c.Err)
+ c.Err.Where = r.URL.Path
+
+ // Block out detailed error when not in developer mode
+ if !*utils.Cfg.ServiceSettings.EnableDeveloper {
+ c.Err.DetailedError = ""
+ }
+
+ w.WriteHeader(c.Err.StatusCode)
+ w.Write([]byte(c.Err.ToJson()))
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementHttpError()
+ }
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementHttpRequest()
+
+ if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" {
+ elapsed := float64(time.Since(now)) / float64(time.Second)
+ einterfaces.GetMetricsInterface().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 := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
+ c.LogError(r.Err)
+ }
+}
+
+func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
+
+ if len(c.Session.UserId) > 0 {
+ extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId)
+ }
+
+ audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
+ if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
+ c.LogError(r.Err)
+ }
+}
+
+func (c *Context) LogError(err *model.AppError) {
+
+ // filter out endless reconnects
+ if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
+ c.LogDebug(err)
+ } else {
+ l4g.Error(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode,
+ c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError)
+ }
+}
+
+func (c *Context) LogDebug(err *model.AppError) {
+ l4g.Debug(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode,
+ c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError)
+}
+
+func (c *Context) IsSystemAdmin() bool {
+ return app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
+}
+
+func (c *Context) SessionRequired() {
+ if len(c.Session.UserId) == 0 {
+ c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired")
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ }
+}
+
+func (c *Context) MfaRequired() {
+ // Must be licensed for MFA and have it configured for enforcement
+ if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication {
+ return
+ }
+
+ // OAuth integrations are excepted
+ if c.Session.IsOAuth {
+ return
+ }
+
+ if user, err := app.GetUser(c.Session.UserId); err != nil {
+ c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired")
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ } else {
+ // Only required for email and ldap accounts
+ if user.AuthService != "" &&
+ user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&
+ user.AuthService != model.USER_AUTH_SERVICE_LDAP {
+ return
+ }
+
+ if !user.MfaActive {
+ c.Err = model.NewLocAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired")
+ c.Err.StatusCode = http.StatusUnauthorized
+ return
+ }
+ }
+}
+
+func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
+ cookie := &http.Cookie{
+ Name: model.SESSION_COOKIE_TOKEN,
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, cookie)
+}
+
+func (c *Context) SetInvalidParam(parameter string) {
+ c.Err = NewInvalidParamError(parameter)
+}
+
+func (c *Context) SetInvalidUrlParam(parameter string) {
+ c.Err = NewInvalidUrlParamError(parameter)
+}
+
+func NewInvalidParamError(parameter string) *model.AppError {
+ err := model.NewLocAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+}
+func NewInvalidUrlParamError(parameter string) *model.AppError {
+ err := model.NewLocAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+}
+
+func (c *Context) SetPermissionError(permission *model.Permission) {
+ c.Err = model.NewLocAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id)
+ c.Err.StatusCode = http.StatusForbidden
+}
+
+func (c *Context) SetSiteURL(url string) {
+ c.siteURL = strings.TrimRight(url, "/")
+}
+
+func (c *Context) GetSiteURL() string {
+ return c.siteURL
+}
+
+func (c *Context) RequireUserId() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.UserId) != 26 {
+ c.SetInvalidUrlParam("user_id")
+ }
+ return c
+}
+
+func (c *Context) RequireTeamId() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.TeamId) != 26 {
+ c.SetInvalidUrlParam("team_id")
+ }
+ return c
+}
+
+func (c *Context) RequireChannelId() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.ChannelId) != 26 {
+ c.SetInvalidUrlParam("channel_id")
+ }
+ return c
+}
diff --git a/api4/params.go b/api4/params.go
new file mode 100644
index 000000000..452b9ba21
--- /dev/null
+++ b/api4/params.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+)
+
+type ApiParams struct {
+ UserId string
+ TeamId string
+ ChannelId string
+ PostId string
+ FileId string
+ CommandId string
+ HookId string
+ EmojiId string
+}
+
+func ApiParamsFromRequest(r *http.Request) *ApiParams {
+ params := &ApiParams{}
+
+ props := mux.Vars(r)
+
+ if val, ok := props["user_id"]; ok {
+ params.UserId = val
+ }
+
+ if val, ok := props["team_id"]; ok {
+ params.TeamId = val
+ }
+
+ if val, ok := props["channel_id"]; ok {
+ params.ChannelId = val
+ }
+
+ if val, ok := props["post_id"]; ok {
+ params.PostId = val
+ }
+
+ if val, ok := props["file_id"]; ok {
+ params.FileId = val
+ }
+
+ if val, ok := props["command_id"]; ok {
+ params.CommandId = val
+ }
+
+ if val, ok := props["hook_id"]; ok {
+ params.HookId = val
+ }
+
+ if val, ok := props["emoji_id"]; ok {
+ params.EmojiId = val
+ }
+
+ return params
+}
diff --git a/api4/user.go b/api4/user.go
new file mode 100644
index 000000000..9d38df1a1
--- /dev/null
+++ b/api4/user.go
@@ -0,0 +1,169 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func InitUser() {
+ l4g.Debug(utils.T("api.user.init.debug"))
+
+ BaseRoutes.Users.Handle("", ApiHandler(createUser)).Methods("POST")
+ BaseRoutes.User.Handle("", ApiSessionRequired(getUser)).Methods("GET")
+ BaseRoutes.User.Handle("", ApiSessionRequired(updateUser)).Methods("PUT")
+
+ BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST")
+ BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST")
+
+}
+
+func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
+ user := model.UserFromJson(r.Body)
+ if user == nil {
+ c.SetInvalidParam("user")
+ return
+ }
+
+ hash := r.URL.Query().Get("h")
+ inviteId := r.URL.Query().Get("iid")
+
+ // No permission check required
+
+ var ruser *model.User
+ var err *model.AppError
+ if len(hash) > 0 {
+ ruser, err = app.CreateUserWithHash(user, hash, r.URL.Query().Get("d"))
+ } else if len(inviteId) > 0 {
+ ruser, err = app.CreateUserWithInviteId(user, inviteId, c.GetSiteURL())
+ } else {
+ ruser, err = app.CreateUserFromSignup(user, c.GetSiteURL())
+ }
+
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ w.Write([]byte(ruser.ToJson()))
+}
+
+func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireUserId()
+ if c.Err != nil {
+ return
+ }
+
+ // No permission check required
+
+ var user *model.User
+ var err *model.AppError
+
+ if user, err = app.GetUser(c.Params.UserId); err != nil {
+ c.Err = err
+ return
+ }
+
+ etag := user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)
+
+ if HandleEtag(etag, "Get User", w, r) {
+ return
+ } else {
+ app.SanitizeProfile(user, c.IsSystemAdmin())
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.Write([]byte(user.ToJson()))
+ return
+ }
+}
+
+func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireUserId()
+ if c.Err != nil {
+ return
+ }
+
+ user := model.UserFromJson(r.Body)
+ if user == nil {
+ c.SetInvalidParam("user")
+ return
+ }
+
+ if !app.SessionHasPermissionToUser(c.Session, user.Id) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
+ return
+ }
+
+ if ruser, err := app.UpdateUserAsUser(user, c.GetSiteURL(), c.IsSystemAdmin()); err != nil {
+ c.Err = err
+ return
+ } else {
+ c.LogAudit("")
+ w.Write([]byte(ruser.ToJson()))
+ }
+}
+
+func login(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ id := props["id"]
+ loginId := props["login_id"]
+ password := props["password"]
+ mfaToken := props["token"]
+ deviceId := props["device_id"]
+ ldapOnly := props["ldap_only"] == "true"
+
+ c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
+ user, err := app.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
+ if err != nil {
+ c.LogAuditWithUserId(id, "failure - login_id="+loginId)
+ c.Err = err
+ return
+ }
+
+ c.LogAuditWithUserId(user.Id, "authenticated")
+
+ var session *model.Session
+ session, err = app.DoLogin(w, r, user, deviceId)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ c.LogAuditWithUserId(user.Id, "success")
+
+ c.Session = *session
+
+ user.Sanitize(map[string]bool{})
+
+ w.Write([]byte(user.ToJson()))
+}
+
+func logout(c *Context, w http.ResponseWriter, r *http.Request) {
+ data := make(map[string]string)
+ data["user_id"] = c.Session.UserId
+
+ Logout(c, w, r)
+ if c.Err == nil {
+ w.Write([]byte(model.MapToJson(data)))
+ }
+}
+
+func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.LogAudit("")
+ c.RemoveSessionCookie(w, r)
+ if c.Session.Id != "" {
+ if err := app.RevokeSessionById(c.Session.Id); err != nil {
+ c.Err = err
+ return
+ }
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/user_test.go b/api4/user_test.go
new file mode 100644
index 000000000..d643f4e3a
--- /dev/null
+++ b/api4/user_test.go
@@ -0,0 +1,186 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api4
+
+import (
+ "net/http"
+ "strconv"
+ "testing"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func TestCreateUser(t *testing.T) {
+ th := Setup()
+ Client := th.Client
+
+ user := model.User{Email: GenerateTestEmail(), Nickname: "Corey Hulen", Password: "hello1", Username: GenerateTestUsername(), Roles: model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id}
+
+ ruser, resp := Client.CreateUser(&user)
+ CheckNoError(t, resp)
+
+ Client.Login(user.Email, user.Password)
+
+ if ruser.Nickname != user.Nickname {
+ t.Fatal("nickname didn't match")
+ }
+
+ if ruser.Roles != model.ROLE_SYSTEM_USER.Id {
+ t.Fatal("did not clear roles")
+ }
+
+ CheckUserSanitization(t, ruser)
+
+ _, resp = Client.CreateUser(ruser)
+ CheckBadRequestStatus(t, resp)
+
+ ruser.Id = ""
+ ruser.Username = GenerateTestUsername()
+ ruser.Password = "passwd1"
+ _, resp = Client.CreateUser(ruser)
+ CheckErrorMessage(t, resp, "An account with that email already exists.")
+ CheckBadRequestStatus(t, resp)
+
+ ruser.Email = GenerateTestEmail()
+ ruser.Username = user.Username
+ _, resp = Client.CreateUser(ruser)
+ CheckErrorMessage(t, resp, "An account with that username already exists.")
+ CheckBadRequestStatus(t, resp)
+
+ ruser.Email = ""
+ _, resp = Client.CreateUser(ruser)
+ CheckErrorMessage(t, resp, "Invalid email")
+ CheckBadRequestStatus(t, resp)
+
+ if r, err := Client.DoApiPost("/users", "garbage"); err == nil {
+ t.Fatal("should have errored")
+ } else {
+ if r.StatusCode != http.StatusBadRequest {
+ t.Log("actual: " + strconv.Itoa(r.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
+ t.Fatal("wrong status code")
+ }
+ }
+}
+
+func TestGetUser(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ Client := th.Client
+
+ user := th.CreateUser()
+
+ ruser, resp := Client.GetUser(user.Id, "")
+ CheckNoError(t, resp)
+ CheckUserSanitization(t, ruser)
+
+ if ruser.Email != user.Email {
+ t.Fatal("emails did not match")
+ }
+
+ ruser, resp = Client.GetUser(user.Id, resp.Etag)
+ CheckEtag(t, ruser, resp)
+
+ _, resp = Client.GetUser("junk", "")
+ CheckBadRequestStatus(t, resp)
+
+ _, resp = Client.GetUser(model.NewId(), "")
+ CheckNotFoundStatus(t, resp)
+
+ // Check against privacy config settings
+ emailPrivacy := utils.Cfg.PrivacySettings.ShowEmailAddress
+ namePrivacy := utils.Cfg.PrivacySettings.ShowFullName
+ defer func() {
+ utils.Cfg.PrivacySettings.ShowEmailAddress = emailPrivacy
+ utils.Cfg.PrivacySettings.ShowFullName = namePrivacy
+ }()
+ utils.Cfg.PrivacySettings.ShowEmailAddress = false
+ utils.Cfg.PrivacySettings.ShowFullName = false
+
+ ruser, resp = Client.GetUser(user.Id, "")
+ CheckNoError(t, resp)
+
+ if ruser.Email != "" {
+ t.Fatal("email should be blank")
+ }
+ if ruser.FirstName != "" {
+ t.Fatal("first name should be blank")
+ }
+ if ruser.LastName != "" {
+ t.Fatal("last name should be blank")
+ }
+
+ Client.Logout()
+ _, resp = Client.GetUser(user.Id, "")
+ CheckUnauthorizedStatus(t, resp)
+
+ // System admins should ignore privacy settings
+ th.LoginSystemAdmin()
+ ruser, resp = Client.GetUser(user.Id, resp.Etag)
+ if ruser.Email == "" {
+ t.Fatal("email should not be blank")
+ }
+ if ruser.FirstName == "" {
+ t.Fatal("first name should not be blank")
+ }
+ if ruser.LastName == "" {
+ t.Fatal("last name should not be blank")
+ }
+}
+
+func TestUpdateUser(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ Client := th.Client
+
+ user := th.CreateUser()
+ Client.Login(user.Email, user.Password)
+
+ user.Nickname = "Joram Wilander"
+ user.Roles = model.ROLE_SYSTEM_ADMIN.Id
+ user.LastPasswordUpdate = 123
+
+ ruser, resp := Client.UpdateUser(user)
+ CheckNoError(t, resp)
+ CheckUserSanitization(t, ruser)
+
+ if ruser.Nickname != "Joram Wilander" {
+ t.Fatal("Nickname did not update properly")
+ }
+ if ruser.Roles != model.ROLE_SYSTEM_USER.Id {
+ t.Fatal("Roles should not have updated")
+ }
+ if ruser.LastPasswordUpdate == 123 {
+ t.Fatal("LastPasswordUpdate should not have updated")
+ }
+
+ ruser.Id = "junk"
+ _, resp = Client.UpdateUser(ruser)
+ CheckBadRequestStatus(t, resp)
+
+ ruser.Id = model.NewId()
+ _, resp = Client.UpdateUser(ruser)
+ CheckForbiddenStatus(t, resp)
+
+ if r, err := Client.DoApiPut("/users/"+ruser.Id, "garbage"); err == nil {
+ t.Fatal("should have errored")
+ } else {
+ if r.StatusCode != http.StatusBadRequest {
+ t.Log("actual: " + strconv.Itoa(r.StatusCode))
+ t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
+ t.Fatal("wrong status code")
+ }
+ }
+
+ Client.Logout()
+ _, resp = Client.UpdateUser(user)
+ CheckUnauthorizedStatus(t, resp)
+
+ th.LoginBasic()
+ _, resp = Client.UpdateUser(user)
+ CheckForbiddenStatus(t, resp)
+
+ th.LoginSystemAdmin()
+ _, resp = Client.UpdateUser(user)
+ CheckNoError(t, resp)
+}
diff --git a/api/authentication.go b/app/authentication.go
index ab649ee10..0561ff821 100644
--- a/api/authentication.go
+++ b/app/authentication.go
@@ -1,10 +1,9 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
- "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -13,8 +12,8 @@ import (
"strings"
)
-func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
- if err := checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
+func CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
+ if err := CheckUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
@@ -40,13 +39,13 @@ func doubleCheckPassword(user *model.User, password string) *model.AppError {
func checkUserPassword(user *model.User, password string) *model.AppError {
if !model.ComparePassword(user.Password, password) {
- if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
+ if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
return result.Err
}
return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
} else {
- if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
+ if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
return result.Err
}
@@ -71,7 +70,7 @@ func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaTok
user = ldapUser
}
- if err := checkUserMfa(user, mfaToken); err != nil {
+ if err := CheckUserMfa(user, mfaToken); err != nil {
return nil, err
}
@@ -83,8 +82,8 @@ func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaTok
return user, nil
}
-func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
- if err := checkUserMfa(user, mfaToken); err != nil {
+func CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
+ if err := CheckUserMfa(user, mfaToken); err != nil {
return err
}
@@ -103,7 +102,7 @@ func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string
return nil
}
-func checkUserMfa(user *model.User, token string) *model.AppError {
+func CheckUserMfa(user *model.User, token string) *model.AppError {
if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
return nil
}
@@ -168,7 +167,7 @@ func authenticateUser(user *model.User, password, mfaToken string) (*model.User,
err.StatusCode = http.StatusBadRequest
return user, err
} else {
- if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
+ if err := CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
} else {
diff --git a/app/file.go b/app/file.go
index a4419bde8..4ddf7ac2d 100644
--- a/app/file.go
+++ b/app/file.go
@@ -5,13 +5,13 @@ package app
import (
"bytes"
- _ "image/gif"
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
+ _ "image/gif"
"image/jpeg"
"io"
"io/ioutil"
@@ -24,9 +24,9 @@ import (
"sync"
l4g "github.com/alecthomas/log4go"
+ "github.com/disintegration/imaging"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "github.com/disintegration/imaging"
s3 "github.com/minio/minio-go"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
@@ -359,7 +359,7 @@ func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
- return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
+ return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX_V3, info.Id, hash)
}
func GeneratePublicLinkHash(fileId, salt string) string {
diff --git a/app/login.go b/app/login.go
new file mode 100644
index 000000000..e9bcf1f03
--- /dev/null
+++ b/app/login.go
@@ -0,0 +1,136 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "github.com/mssola/user_agent"
+)
+
+func AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) {
+ if len(password) == 0 {
+ err := model.NewLocAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ var user *model.User
+ var err *model.AppError
+
+ if len(id) != 0 {
+ if user, err = GetUser(id); err != nil {
+ err.StatusCode = http.StatusBadRequest
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementLoginFail()
+ }
+ return nil, err
+ }
+ } else {
+ if user, err = GetUserForLogin(loginId, ldapOnly); err != nil {
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementLoginFail()
+ }
+ return nil, err
+ }
+ }
+
+ // and then authenticate them
+ if user, err = authenticateUser(user, password, mfaToken); err != nil {
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementLoginFail()
+ }
+ return nil, err
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementLogin()
+ }
+
+ return user, nil
+}
+
+func DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) (*model.Session, *model.AppError) {
+ session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false}
+
+ maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24
+
+ if len(deviceId) > 0 {
+ session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
+
+ // A special case where we logout of all other sessions with the same Id
+ if err := RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
+ err.StatusCode = http.StatusInternalServerError
+ return nil, err
+ }
+ } else {
+ session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
+ }
+
+ ua := user_agent.New(r.UserAgent())
+
+ plat := ua.Platform()
+ if plat == "" {
+ plat = "unknown"
+ }
+
+ os := ua.OS()
+ if os == "" {
+ os = "unknown"
+ }
+
+ bname, bversion := ua.Browser()
+ if bname == "" {
+ bname = "unknown"
+ }
+
+ if bversion == "" {
+ bversion = "0.0"
+ }
+
+ session.AddProp(model.SESSION_PROP_PLATFORM, plat)
+ session.AddProp(model.SESSION_PROP_OS, os)
+ session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
+
+ var err *model.AppError
+ if session, err = CreateSession(session); err != nil {
+ err.StatusCode = http.StatusInternalServerError
+ return nil, err
+ }
+
+ w.Header().Set(model.HEADER_TOKEN, session.Token)
+
+ secure := false
+ if GetProtocol(r) == "https" {
+ secure = true
+ }
+
+ expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
+ sessionCookie := &http.Cookie{
+ Name: model.SESSION_COOKIE_TOKEN,
+ Value: session.Token,
+ Path: "/",
+ MaxAge: maxAge,
+ Expires: expiresAt,
+ HttpOnly: true,
+ Secure: secure,
+ }
+
+ http.SetCookie(w, sessionCookie)
+
+ return session, nil
+}
+
+func GetProtocol(r *http.Request) string {
+ if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
+ return "https"
+ } else {
+ return "http"
+ }
+}
diff --git a/app/user.go b/app/user.go
index 8fbed301d..848f7c9fc 100644
--- a/app/user.go
+++ b/app/user.go
@@ -31,6 +31,10 @@ import (
)
func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
+ if err := IsUserSignUpAllowed(); err != nil {
+ return nil, err
+ }
+
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
@@ -69,6 +73,10 @@ func CreateUserWithHash(user *model.User, hash string, data string) (*model.User
}
func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (*model.User, *model.AppError) {
+ if err := IsUserSignUpAllowed(); err != nil {
+ return nil, err
+ }
+
var team *model.Team
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
return nil, result.Err
@@ -76,6 +84,8 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
team = result.Data.(*model.Team)
}
+ user.EmailVerified = false
+
var ruser *model.User
var err *model.AppError
if ruser, err = CreateUser(user); err != nil {
@@ -95,6 +105,40 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
return ruser, nil
}
+func CreateUserFromSignup(user *model.User, siteURL string) (*model.User, *model.AppError) {
+ if err := IsUserSignUpAllowed(); err != nil {
+ return nil, err
+ }
+
+ if !IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
+ err := model.NewLocAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email)
+ err.StatusCode = http.StatusForbidden
+ return nil, err
+ }
+
+ user.EmailVerified = false
+
+ ruser, err := CreateUser(user)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+
+ return ruser, nil
+}
+
+func IsUserSignUpAllowed() *model.AppError {
+ if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation {
+ err := model.NewLocAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+ return nil
+}
+
func IsFirstUserAccount() bool {
if SessionCacheLength() == 0 {
if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
@@ -575,6 +619,43 @@ func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppE
return nil
}
+func UpdatePasswordAsUser(userId, currentPassword, newPassword, siteURL string) *model.AppError {
+ var user *model.User
+ var err *model.AppError
+
+ if user, err = GetUser(userId); err != nil {
+ return err
+ }
+
+ if user == nil {
+ err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if user.AuthData != nil && *user.AuthData != "" {
+ err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if err := doubleCheckPassword(user, currentPassword); err != nil {
+ if err.Id == "api.user.check_user_password.invalid.app_error" {
+ err = model.NewLocAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "")
+ }
+ err.StatusCode = http.StatusForbidden
+ return err
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ if err := UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu"), siteURL); err != nil {
+ return err
+ }
+
+ return nil
+}
+
func UpdateActiveNoLdap(userId string, active bool) (*model.User, *model.AppError) {
var user *model.User
var err *model.AppError
@@ -624,6 +705,33 @@ func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError)
}
}
+func SanitizeProfile(user *model.User, asAdmin bool) {
+ options := utils.Cfg.GetSanitizeOptions()
+ if asAdmin {
+ options["email"] = true
+ options["fullname"] = true
+ options["authservice"] = true
+ }
+ user.SanitizeProfile(options)
+}
+
+func UpdateUserAsUser(user *model.User, siteURL string, asAdmin bool) (*model.User, *model.AppError) {
+ updatedUser, err := UpdateUser(user, siteURL)
+ if err != nil {
+ return nil, err
+ }
+
+ SanitizeProfile(updatedUser, asAdmin)
+
+ omitUsers := make(map[string]bool, 1)
+ omitUsers[updatedUser.Id] = true
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
+ message.Add("user", updatedUser)
+ go Publish(message)
+
+ return updatedUser, nil
+}
+
func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) {
if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
return nil, result.Err
diff --git a/cmd/platform/server.go b/cmd/platform/server.go
index 560403a6b..1aade29dd 100644
--- a/cmd/platform/server.go
+++ b/cmd/platform/server.go
@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/api4"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/manualtesting"
@@ -73,6 +74,7 @@ func runServer(configFileLocation string) {
app.NewServer()
app.InitStores()
api.InitRouter()
+ api4.InitApi(false)
api.InitApi()
web.InitWeb()
diff --git a/glide.lock b/glide.lock
index d91c3c347..054dd5532 100644
--- a/glide.lock
+++ b/glide.lock
@@ -40,7 +40,7 @@ imports:
- name: github.com/gorilla/handlers
version: e1b2144f2167de0e1042d1d35e5cba5119d4fb5d
- name: github.com/gorilla/mux
- version: 757bef944d0f21880861c2dd9c871ca543023cba
+ version: 34dda716af12ba79ed40ce1f66b102ff75dc3411
- name: github.com/gorilla/websocket
version: e8f0f8aaa98dfb6586cbdf2978d511e3199a960a
- name: github.com/hashicorp/golang-lru
diff --git a/glide.yaml b/glide.yaml
index ae0560199..85ef3a2fc 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -24,7 +24,7 @@ import:
- package: github.com/gorilla/handlers
version: e1b2144f2167de0e1042d1d35e5cba5119d4fb5d
- package: github.com/gorilla/mux
- version: 757bef944d0f21880861c2dd9c871ca543023cba
+ version: 34dda716af12ba79ed40ce1f66b102ff75dc3411
- package: github.com/gorilla/websocket
version: e8f0f8aaa98dfb6586cbdf2978d511e3199a960a
- package: github.com/lib/pq
diff --git a/i18n/en.json b/i18n/en.json
index 355fc9945..4b254a42c 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -800,6 +800,14 @@
"translation": "Invalid {{.Name}} parameter"
},
{
+ "id": "api.context.invalid_body_param.app_error",
+ "translation": "Invalid or missing {{.Name}} in request body"
+ },
+ {
+ "id": "api.context.invalid_url_param.app_error",
+ "translation": "Invalid or missing {{.Name}} parameter in request URL"
+ },
+ {
"id": "api.context.invalid_session.error",
"translation": "Invalid session err=%v"
},
@@ -5245,7 +5253,7 @@
},
{
"id": "store.sql_user.missing_account.const",
- "translation": "We couldn't find an existing account matching your email address for this team. This team may require an invite from the team owner to join."
+ "translation": "We couldn't find the user."
},
{
"id": "store.sql_user.permanent_delete.app_error",
diff --git a/model/client.go b/model/client.go
index c75121e97..52e1ae989 100644
--- a/model/client.go
+++ b/model/client.go
@@ -40,7 +40,8 @@ const (
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
- API_URL_SUFFIX = API_URL_SUFFIX_V3
+ API_URL_SUFFIX_V4 = "/api/v4"
+ API_URL_SUFFIX = API_URL_SUFFIX_V4
)
type Result struct {
@@ -71,7 +72,7 @@ type Client struct {
// NewClient constructs a new client with convienence methods for talking to
// the server.
func NewClient(url string) *Client {
- return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", "", "", "", ""}
+ return &Client{url, url + API_URL_SUFFIX_V3, &http.Client{}, "", "", "", "", "", ""}
}
func closeBody(r *http.Response) {
diff --git a/model/client4.go b/model/client4.go
new file mode 100644
index 000000000..3c585fbe4
--- /dev/null
+++ b/model/client4.go
@@ -0,0 +1,222 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+type Response struct {
+ StatusCode int
+ Error *AppError
+ RequestId string
+ Etag string
+ ServerVersion string
+}
+
+type Client4 struct {
+ Url string // The location of the server, for example "http://localhost:8065"
+ ApiUrl string // The api location of the server, for example "http://localhost:8065/api/v4"
+ HttpClient *http.Client // The http client
+ AuthToken string
+ AuthType string
+}
+
+func NewAPIv4Client(url string) *Client4 {
+ return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", ""}
+}
+
+func BuildResponse(r *http.Response) *Response {
+ return &Response{
+ StatusCode: r.StatusCode,
+ RequestId: r.Header.Get(HEADER_REQUEST_ID),
+ Etag: r.Header.Get(HEADER_ETAG_SERVER),
+ ServerVersion: r.Header.Get(HEADER_VERSION_ID),
+ }
+}
+
+func (c *Client4) SetOAuthToken(token string) {
+ c.AuthToken = token
+ c.AuthType = HEADER_TOKEN
+}
+
+func (c *Client4) ClearOAuthToken() {
+ c.AuthToken = ""
+ c.AuthType = HEADER_BEARER
+}
+
+func (c *Client4) GetUsersRoute() string {
+ return fmt.Sprintf("/users")
+}
+
+func (c *Client4) GetUserRoute(userId string) string {
+ return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
+}
+
+func (c *Client4) GetTeamsRoute() string {
+ return fmt.Sprintf("/teams")
+}
+
+func (c *Client4) GetTeamRoute(teamId string) string {
+ return fmt.Sprintf(c.GetTeamsRoute()+"/%v", teamId)
+}
+
+func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
+ return c.DoApiRequest(http.MethodGet, url, "", etag)
+}
+
+func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) {
+ return c.DoApiRequest(http.MethodPost, url, data, "")
+}
+
+func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) {
+ return c.DoApiRequest(http.MethodPut, url, data, "")
+}
+
+func (c *Client4) DoApiDelete(url string, data string) (*http.Response, *AppError) {
+ return c.DoApiRequest(http.MethodDelete, url, "", "")
+}
+
+func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) {
+ rq, _ := http.NewRequest(method, c.ApiUrl+url, strings.NewReader(data))
+ rq.Close = true
+
+ if len(etag) > 0 {
+ rq.Header.Set(HEADER_ETAG_CLIENT, etag)
+ }
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
+ } else if rp.StatusCode == 304 {
+ return rp, nil
+ } else if rp.StatusCode >= 300 {
+ defer closeBody(rp)
+ return rp, AppErrorFromJson(rp.Body)
+ } else {
+ return rp, nil
+ }
+}
+
+// CheckStatusOK is a convenience function for checking the standard OK response
+// from the web service.
+func CheckStatusOK(r *http.Response) bool {
+ m := MapFromJson(r.Body)
+ defer closeBody(r)
+
+ if m != nil && m[STATUS] == STATUS_OK {
+ return true
+ }
+
+ return false
+}
+
+// Authentication Section
+
+// LoginById authenticates a user by user id and password.
+func (c *Client4) LoginById(id string, password string) (*User, *Response) {
+ m := make(map[string]string)
+ m["id"] = id
+ m["password"] = password
+ return c.login(m)
+}
+
+// Login authenticates a user by login id, which can be username, email or some sort
+// of SSO identifier based on server configuration, and a password.
+func (c *Client4) Login(loginId string, password string) (*User, *Response) {
+ m := make(map[string]string)
+ m["login_id"] = loginId
+ m["password"] = password
+ return c.login(m)
+}
+
+// LoginByLdap authenticates a user by LDAP id and password.
+func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response) {
+ m := make(map[string]string)
+ m["login_id"] = loginId
+ m["password"] = password
+ m["ldap_only"] = "true"
+ return c.login(m)
+}
+
+// LoginWithDevice authenticates a user by login id (username, email or some sort
+// of SSO identifier based on configuration), password and attaches a device id to
+// the session.
+func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response) {
+ m := make(map[string]string)
+ m["login_id"] = loginId
+ m["password"] = password
+ m["device_id"] = deviceId
+ return c.login(m)
+}
+
+func (c *Client4) login(m map[string]string) (*User, *Response) {
+ if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil {
+ return nil, &Response{StatusCode: r.StatusCode, Error: err}
+ } else {
+ c.AuthToken = r.Header.Get(HEADER_TOKEN)
+ c.AuthType = HEADER_BEARER
+ defer closeBody(r)
+ return UserFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// Logout terminates the current user's session.
+func (c *Client4) Logout() (bool, *Response) {
+ if r, err := c.DoApiPost("/users/logout", ""); err != nil {
+ return false, &Response{StatusCode: r.StatusCode, Error: err}
+ } else {
+ c.AuthToken = ""
+ c.AuthType = HEADER_BEARER
+
+ defer closeBody(r)
+ return CheckStatusOK(r), BuildResponse(r)
+ }
+}
+
+// User Section
+
+// CreateUser creates a user in the system based on the provided user struct.
+func (c *Client4) CreateUser(user *User) (*User, *Response) {
+ if r, err := c.DoApiPost(c.GetUsersRoute(), user.ToJson()); err != nil {
+ return nil, &Response{StatusCode: r.StatusCode, Error: err}
+ } else {
+ defer closeBody(r)
+ return UserFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// GetUser returns a user based on the provided user id string.
+func (c *Client4) GetUser(userId, etag string) (*User, *Response) {
+ if r, err := c.DoApiGet(c.GetUserRoute(userId), etag); err != nil {
+ return nil, &Response{StatusCode: r.StatusCode, Error: err}
+ } else {
+ defer closeBody(r)
+ return UserFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// UpdateUser updates a user in the system based on the provided user struct.
+func (c *Client4) UpdateUser(user *User) (*User, *Response) {
+ if r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()); err != nil {
+ return nil, &Response{StatusCode: r.StatusCode, Error: err}
+ } else {
+ defer closeBody(r)
+ return UserFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// Team Section
+// to be filled in..
+
+// Channel Section
+// to be filled in..
+
+// Post Section
+// to be filled in..
diff --git a/model/user.go b/model/user.go
index 876ba70e7..49963bb41 100644
--- a/model/user.go
+++ b/model/user.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "net/http"
"regexp"
"strings"
"unicode/utf8"
@@ -56,51 +57,51 @@ type User struct {
func (u *User) IsValid() *AppError {
if len(u.Id) != 26 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "")
+ return NewAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if u.CreateAt == 0 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.UpdateAt == 0 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if !IsValidUsername(u.Username) {
- return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if len(u.Email) > 128 || len(u.Email) == 0 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.Nickname) > 64 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.Position) > 35 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.FirstName) > 64 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.LastName) > 64 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.AuthData != nil && len(*u.AuthData) > 128 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 {
- return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
+ return NewAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
return nil
diff --git a/model/utils.go b/model/utils.go
index 0ce243fe7..05143b20d 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -93,6 +93,18 @@ func AppErrorFromJson(data io.Reader) *AppError {
}
}
+func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError {
+ ap := &AppError{}
+ ap.Id = id
+ ap.params = params
+ ap.Message = id
+ ap.Where = where
+ ap.DetailedError = details
+ ap.StatusCode = status
+ ap.IsOAuth = false
+ return ap
+}
+
func NewLocAppError(where string, id string, params map[string]interface{}, details string) *AppError {
ap := &AppError{}
ap.Id = id
diff --git a/model/websocket_client.go b/model/websocket_client.go
index c91855134..083fe110a 100644
--- a/model/websocket_client.go
+++ b/model/websocket_client.go
@@ -26,14 +26,14 @@ type WebSocketClient struct {
// NewWebSocketClient constructs a new WebSocket client with convienence
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
- conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", nil)
+ conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
client := &WebSocketClient{
url,
- url + API_URL_SUFFIX,
+ url + API_URL_SUFFIX_V3,
conn,
authToken,
1,
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 09742a4f4..02cbb3fbf 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -7,6 +7,7 @@ import (
"crypto/md5"
"database/sql"
"fmt"
+ "net/http"
"strconv"
"strings"
@@ -108,9 +109,9 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel {
if err := us.GetMaster().Insert(user); err != nil {
if IsUniqueConstraintError(err.Error(), []string{"Email", "users_email_key", "idx_users_email_unique"}) {
- result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.email_exists.app_error", nil, "user_id="+user.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlUserStore.Save", "store.sql_user.save.email_exists.app_error", nil, "user_id="+user.Id+", "+err.Error(), http.StatusBadRequest)
} else if IsUniqueConstraintError(err.Error(), []string{"Username", "users_username_key", "idx_users_username_unique"}) {
- result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.username_exists.app_error", nil, "user_id="+user.Id+", "+err.Error())
+ result.Err = model.NewAppError("SqlUserStore.Save", "store.sql_user.save.username_exists.app_error", nil, "user_id="+user.Id+", "+err.Error(), http.StatusBadRequest)
} else {
result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.app_error", nil, "user_id="+user.Id+", "+err.Error())
}
@@ -389,6 +390,7 @@ func (us SqlUserStore) Get(id string) StoreChannel {
result.Err = model.NewLocAppError("SqlUserStore.Get", "store.sql_user.get.app_error", nil, "user_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewLocAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id)
+ result.Err.StatusCode = http.StatusNotFound
} else {
result.Data = obj.(*model.User)
}
diff --git a/utils/password.go b/utils/password.go
index dc1d771b8..b129869d3 100644
--- a/utils/password.go
+++ b/utils/password.go
@@ -4,8 +4,10 @@
package utils
import (
- "github.com/mattermost/platform/model"
+ "net/http"
"strings"
+
+ "github.com/mattermost/platform/model"
)
func IsPasswordValid(password string) *model.AppError {
@@ -57,7 +59,7 @@ func IsPasswordValid(password string) *model.AppError {
}
if isError {
- return model.NewLocAppError("User.IsValid", id+".app_error", map[string]interface{}{"Min": min}, "")
+ return model.NewAppError("User.IsValid", id+".app_error", map[string]interface{}{"Min": min}, "", http.StatusBadRequest)
}
return nil
diff --git a/vendor/github.com/gorilla/mux/mux_test.go b/vendor/github.com/gorilla/mux/mux_test.go
index 39a099c1e..b4b049efc 100644
--- a/vendor/github.com/gorilla/mux/mux_test.go
+++ b/vendor/github.com/gorilla/mux/mux_test.go
@@ -1017,6 +1017,9 @@ func TestBuildVarsFunc(t *testing.T) {
func TestSubRouter(t *testing.T) {
subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter()
subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter()
+ subrouter3 := new(Route).PathPrefix("/foo").Subrouter()
+ subrouter4 := new(Route).PathPrefix("/foo/bar").Subrouter()
+ subrouter5 := new(Route).PathPrefix("/{category}").Subrouter()
tests := []routeTest{
{
@@ -1057,6 +1060,61 @@ func TestSubRouter(t *testing.T) {
pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: false,
},
+ {
+ route: subrouter3.Path("/"),
+ request: newRequest("GET", "http://localhost/foo/"),
+ vars: map[string]string{},
+ host: "",
+ path: "/foo/",
+ pathTemplate: `/foo/`,
+ shouldMatch: true,
+ },
+ {
+ route: subrouter3.Path(""),
+ request: newRequest("GET", "http://localhost/foo"),
+ vars: map[string]string{},
+ host: "",
+ path: "/foo",
+ pathTemplate: `/foo`,
+ shouldMatch: true,
+ },
+
+ {
+ route: subrouter4.Path("/"),
+ request: newRequest("GET", "http://localhost/foo/bar/"),
+ vars: map[string]string{},
+ host: "",
+ path: "/foo/bar/",
+ pathTemplate: `/foo/bar/`,
+ shouldMatch: true,
+ },
+ {
+ route: subrouter4.Path(""),
+ request: newRequest("GET", "http://localhost/foo/bar"),
+ vars: map[string]string{},
+ host: "",
+ path: "/foo/bar",
+ pathTemplate: `/foo/bar`,
+ shouldMatch: true,
+ },
+ {
+ route: subrouter5.Path("/"),
+ request: newRequest("GET", "http://localhost/baz/"),
+ vars: map[string]string{"category": "baz"},
+ host: "",
+ path: "/baz/",
+ pathTemplate: `/{category}/`,
+ shouldMatch: true,
+ },
+ {
+ route: subrouter5.Path(""),
+ request: newRequest("GET", "http://localhost/baz"),
+ vars: map[string]string{"category": "baz"},
+ host: "",
+ path: "/baz",
+ pathTemplate: `/{category}`,
+ shouldMatch: true,
+ },
}
for _, test := range tests {
diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go
index 293b6d493..922191592 100644
--- a/vendor/github.com/gorilla/mux/route.go
+++ b/vendor/github.com/gorilla/mux/route.go
@@ -153,7 +153,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
}
r.regexp = r.getRegexpGroup()
if !matchHost && !matchQuery {
- if len(tpl) == 0 || tpl[0] != '/' {
+ if tpl == "/" && (len(tpl) == 0 || tpl[0] != '/') {
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
}
if r.regexp.path != nil {