From c01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 30 Jan 2017 08:30:02 -0500 Subject: Implement APIv4 infrastructure (#5191) * Implement APIv4 infrastructure * Update parameter requirement functions per feedback --- Makefile | 4 +- api/api.go | 2 +- api/authentication.go | 178 --------------- api/command_test.go | 4 +- api/context.go | 41 ++-- api/file_test.go | 3 +- api/oauth.go | 10 +- api/user.go | 229 +++---------------- api/user_test.go | 2 +- api4/api.go | 189 ++++++++++++++++ api4/apitestlib.go | 211 +++++++++++++++++ api4/context.go | 360 ++++++++++++++++++++++++++++++ api4/params.go | 61 +++++ api4/user.go | 169 ++++++++++++++ api4/user_test.go | 186 +++++++++++++++ app/authentication.go | 177 +++++++++++++++ app/file.go | 6 +- app/login.go | 136 +++++++++++ app/user.go | 108 +++++++++ cmd/platform/server.go | 2 + glide.lock | 2 +- glide.yaml | 2 +- i18n/en.json | 10 +- model/client.go | 5 +- model/client4.go | 222 ++++++++++++++++++ model/user.go | 25 ++- model/utils.go | 12 + model/websocket_client.go | 4 +- store/sql_user_store.go | 6 +- utils/password.go | 6 +- vendor/github.com/gorilla/mux/mux_test.go | 58 +++++ vendor/github.com/gorilla/mux/route.go | 2 +- 32 files changed, 1993 insertions(+), 439 deletions(-) delete mode 100644 api/authentication.go create mode 100644 api4/api.go create mode 100644 api4/apitestlib.go create mode 100644 api4/context.go create mode 100644 api4/params.go create mode 100644 api4/user.go create mode 100644 api4/user_test.go create mode 100644 app/authentication.go create mode 100644 app/login.go create mode 100644 model/client4.go 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/authentication.go b/api/authentication.go deleted file mode 100644 index ab649ee10..000000000 --- a/api/authentication.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - "github.com/mattermost/platform/app" - "github.com/mattermost/platform/einterfaces" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" - - "net/http" - "strings" -) - -func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { - if err := checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { - return err - } - - if err := checkUserPassword(user, password); err != nil { - return err - } - - return nil -} - -// This to be used for places we check the users password when they are already logged in -func doubleCheckPassword(user *model.User, password string) *model.AppError { - if err := checkUserLoginAttempts(user); err != nil { - return err - } - - if err := checkUserPassword(user, password); err != nil { - return err - } - - return nil -} - -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 { - 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 { - return result.Err - } - - return nil - } -} - -func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) { - ldapInterface := einterfaces.GetLdapInterface() - - if ldapInterface == nil || ldapId == nil { - err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return nil, err - } - - var user *model.User - if ldapUser, err := ldapInterface.DoLogin(*ldapId, password); err != nil { - err.StatusCode = http.StatusUnauthorized - return nil, err - } else { - user = ldapUser - } - - if err := checkUserMfa(user, mfaToken); err != nil { - return nil, err - } - - if err := checkUserNotDisabled(user); err != nil { - return nil, err - } - - // user successfully authenticated - return user, nil -} - -func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError { - if err := checkUserMfa(user, mfaToken); err != nil { - return err - } - - if err := checkEmailVerified(user); err != nil { - return err - } - - if err := checkUserNotDisabled(user); err != nil { - return err - } - - if err := checkUserLoginAttempts(user); err != nil { - return err - } - - return nil -} - -func checkUserMfa(user *model.User, token string) *model.AppError { - if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { - return nil - } - - mfaInterface := einterfaces.GetMfaInterface() - if mfaInterface == nil { - return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") - } - - if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { - return err - } else if !ok { - return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") - } - - return nil -} - -func checkUserLoginAttempts(user *model.User) *model.AppError { - if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { - return model.NewLocAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id) - } - - return nil -} - -func checkEmailVerified(user *model.User) *model.AppError { - if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { - return model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id) - } - return nil -} - -func checkUserNotDisabled(user *model.User) *model.AppError { - if user.DeleteAt > 0 { - return model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id) - } - return nil -} - -func authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { - ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP - - if user.AuthService == model.USER_AUTH_SERVICE_LDAP { - if !ldapAvailable { - err := model.NewLocAppError("login", "api.user.login_ldap.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return user, err - } else if ldapUser, err := checkLdapUserPasswordAndAllCriteria(user.AuthData, password, mfaToken); err != nil { - err.StatusCode = http.StatusUnauthorized - return user, err - } else { - // slightly redundant to get the user again, but we need to get it from the LDAP server - return ldapUser, nil - } - } else if user.AuthService != "" { - authService := user.AuthService - if authService == model.USER_AUTH_SERVICE_SAML || authService == model.USER_AUTH_SERVICE_LDAP { - authService = strings.ToUpper(authService) - } - err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "") - err.StatusCode = http.StatusBadRequest - return user, err - } else { - if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil { - err.StatusCode = http.StatusUnauthorized - return user, err - } else { - return user, nil - } - } -} 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/app/authentication.go b/app/authentication.go new file mode 100644 index 000000000..0561ff821 --- /dev/null +++ b/app/authentication.go @@ -0,0 +1,177 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + + "net/http" + "strings" +) + +func CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { + if err := CheckUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { + return err + } + + if err := checkUserPassword(user, password); err != nil { + return err + } + + return nil +} + +// This to be used for places we check the users password when they are already logged in +func doubleCheckPassword(user *model.User, password string) *model.AppError { + if err := checkUserLoginAttempts(user); err != nil { + return err + } + + if err := checkUserPassword(user, password); err != nil { + return err + } + + return nil +} + +func checkUserPassword(user *model.User, password string) *model.AppError { + if !model.ComparePassword(user.Password, password) { + 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 := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { + return result.Err + } + + return nil + } +} + +func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) { + ldapInterface := einterfaces.GetLdapInterface() + + if ldapInterface == nil || ldapId == nil { + err := model.NewLocAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return nil, err + } + + var user *model.User + if ldapUser, err := ldapInterface.DoLogin(*ldapId, password); err != nil { + err.StatusCode = http.StatusUnauthorized + return nil, err + } else { + user = ldapUser + } + + if err := CheckUserMfa(user, mfaToken); err != nil { + return nil, err + } + + if err := checkUserNotDisabled(user); err != nil { + return nil, err + } + + // user successfully authenticated + return user, nil +} + +func CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError { + if err := CheckUserMfa(user, mfaToken); err != nil { + return err + } + + if err := checkEmailVerified(user); err != nil { + return err + } + + if err := checkUserNotDisabled(user); err != nil { + return err + } + + if err := checkUserLoginAttempts(user); err != nil { + return err + } + + return nil +} + +func CheckUserMfa(user *model.User, token string) *model.AppError { + if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + return nil + } + + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "") + } + + if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil { + return err + } else if !ok { + return model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "") + } + + return nil +} + +func checkUserLoginAttempts(user *model.User) *model.AppError { + if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { + return model.NewLocAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id) + } + + return nil +} + +func checkEmailVerified(user *model.User) *model.AppError { + if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { + return model.NewLocAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id) + } + return nil +} + +func checkUserNotDisabled(user *model.User) *model.AppError { + if user.DeleteAt > 0 { + return model.NewLocAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id) + } + return nil +} + +func authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP + + if user.AuthService == model.USER_AUTH_SERVICE_LDAP { + if !ldapAvailable { + err := model.NewLocAppError("login", "api.user.login_ldap.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return user, err + } else if ldapUser, err := checkLdapUserPasswordAndAllCriteria(user.AuthData, password, mfaToken); err != nil { + err.StatusCode = http.StatusUnauthorized + return user, err + } else { + // slightly redundant to get the user again, but we need to get it from the LDAP server + return ldapUser, nil + } + } else if user.AuthService != "" { + authService := user.AuthService + if authService == model.USER_AUTH_SERVICE_SAML || authService == model.USER_AUTH_SERVICE_LDAP { + authService = strings.ToUpper(authService) + } + err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "") + err.StatusCode = http.StatusBadRequest + return user, err + } else { + if err := CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil { + err.StatusCode = http.StatusUnauthorized + return user, err + } else { + return user, nil + } + } +} 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 @@ -799,6 +799,14 @@ "id": "api.context.invalid_param.app_error", "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 { -- cgit v1.2.3-1-g7c22