diff options
36 files changed, 597 insertions, 1016 deletions
diff --git a/api/apitestlib.go b/api/apitestlib.go index d82dc30be..a7c2a9406 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -200,19 +200,19 @@ func (me *TestHelper) CreatePost(client *model.Client, channel *model.Channel) * func (me *TestHelper) LoginBasic() { utils.DisableDebugLogForTest() - me.BasicClient.Must(me.BasicClient.LoginByEmail(me.BasicTeam.Name, me.BasicUser.Email, me.BasicUser.Password)) + me.BasicClient.Must(me.BasicClient.Login(me.BasicUser.Email, me.BasicUser.Password)) utils.EnableDebugLogForTest() } func (me *TestHelper) LoginBasic2() { utils.DisableDebugLogForTest() - me.BasicClient.Must(me.BasicClient.LoginByEmail(me.BasicTeam.Name, me.BasicUser2.Email, me.BasicUser2.Password)) + me.BasicClient.Must(me.BasicClient.Login(me.BasicUser2.Email, me.BasicUser2.Password)) utils.EnableDebugLogForTest() } func (me *TestHelper) LoginSystemAdmin() { utils.DisableDebugLogForTest() - me.SystemAdminClient.Must(me.SystemAdminClient.LoginByEmail(me.SystemAdminTeam.Name, me.SystemAdminUser.Email, me.SystemAdminUser.Password)) + me.SystemAdminClient.Must(me.SystemAdminClient.Login(me.SystemAdminUser.Email, me.SystemAdminUser.Password)) utils.EnableDebugLogForTest() } diff --git a/api/authentication.go b/api/authentication.go index bab83a720..10ed578e1 100644 --- a/api/authentication.go +++ b/api/authentication.go @@ -7,6 +7,8 @@ import ( "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + + "net/http" ) func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { @@ -37,6 +39,32 @@ func checkUserPassword(user *model.User, password string) *model.AppError { } } +func checkLdapUserPasswordAndAllCriteria(ldapId, password, mfaToken string) (*model.User, *model.AppError) { + ldapInterface := einterfaces.GetLdapInterface() + + if ldapInterface == 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 := checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { + err.StatusCode = http.StatusUnauthorized + return user, 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 @@ -97,3 +125,32 @@ func checkUserNotDisabled(user *model.User) *model.AppError { } return nil } + +func authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil + + 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 != "" { + err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": user.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/channel_benchmark_test.go b/api/channel_benchmark_test.go index 3e7c2882c..569c2dcc0 100644 --- a/api/channel_benchmark_test.go +++ b/api/channel_benchmark_test.go @@ -131,7 +131,7 @@ func BenchmarkJoinChannel(b *testing.B) { user = th.BasicClient.Must(th.BasicClient.CreateUser(user, "")).Data.(*model.User) LinkUserToTeam(user, th.BasicTeam) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - th.BasicClient.LoginByEmail(th.BasicTeam.Name, user.Email, "pwd") + th.BasicClient.Login(user.Email, "pwd") // Benchmark Start b.ResetTimer() diff --git a/api/channel_test.go b/api/channel_test.go index 4c8462e6a..31b201346 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -169,7 +169,7 @@ func TestUpdateChannel(t *testing.T) { } } - Client.LoginByEmail(team.Name, user2.Email, user2.Password) + Client.Login(user2.Email, user2.Password) if _, err := Client.UpdateChannel(upChannel1); err == nil { t.Fatal("Standard User should have failed to update") @@ -432,7 +432,7 @@ func TestJoinChannelById(t *testing.T) { user3 := th.CreateUser(th.BasicClient) LinkUserToTeam(user3, team) - Client.LoginByEmail(team.Name, user3.Email, "pwd") + Client.Login(user3.Email, "pwd") if _, err := Client.JoinChannel(rchannel.Id); err == nil { t.Fatal("shoudn't be able to join direct channel") @@ -462,7 +462,7 @@ func TestJoinChannelByName(t *testing.T) { user3 := th.CreateUser(th.BasicClient) LinkUserToTeam(user3, team) - Client.LoginByEmail(team.Name, user3.Email, "pwd") + Client.Login(user3.Email, "pwd") if _, err := Client.JoinChannelByName(rchannel.Name); err == nil { t.Fatal("shoudn't be able to join direct channel") @@ -518,7 +518,7 @@ func TestDeleteChannel(t *testing.T) { Client.AddChannelMember(channelMadeByCA.Id, userTeamAdmin.Id) - Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") + Client.Login(userTeamAdmin.Email, "pwd") channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) @@ -541,7 +541,7 @@ func TestDeleteChannel(t *testing.T) { userStd := th.CreateUser(th.BasicClient) LinkUserToTeam(userStd, team) - Client.LoginByEmail(team.Name, userStd.Email, userStd.Password) + Client.Login(userStd.Email, userStd.Password) if _, err := Client.JoinChannel(channel1.Id); err == nil { t.Fatal("should have failed to join deleted channel") @@ -606,7 +606,7 @@ func TestGetChannelExtraInfo(t *testing.T) { Client2.SetTeamId(team.Id) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - Client2.LoginByEmail(team.Name, user2.Email, "pwd") + Client2.Login(user2.Email, "pwd") Client2.Must(Client2.JoinChannel(channel1.Id)) if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { @@ -775,7 +775,7 @@ func TestRemoveChannelMember(t *testing.T) { channel2 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - Client.LoginByEmail(team.Name, userStd.Email, userStd.Password) + Client.Login(userStd.Email, userStd.Password) if _, err := Client.RemoveChannelMember(channel2.Id, userStd.Id); err == nil { t.Fatal("Should have errored, user not channel admin") diff --git a/api/command_loadtest.go b/api/command_loadtest.go index b76187960..26306440f 100644 --- a/api/command_loadtest.go +++ b/api/command_loadtest.go @@ -164,7 +164,7 @@ func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message s if err := CreateBasicUser(client); err != nil { return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD) + client.Login(BTEST_USER_EMAIL, BTEST_USER_PASSWORD) environment, err := CreateTestEnvironmentWithTeams( client, utils.Range{numTeams, numTeams}, diff --git a/api/file_test.go b/api/file_test.go index dd4a8520b..015048ec4 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -217,7 +217,7 @@ func TestGetFile(t *testing.T) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) - Client.LoginByEmail(team2.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") Client.SetTeamId(team2.Id) if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t="+team.Id, false); downErr != nil { diff --git a/api/post_test.go b/api/post_test.go index 8556d66b6..529cc6e4d 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -91,7 +91,7 @@ func TestCreatePost(t *testing.T) { t.Fatal("Should have been forbidden") } - Client.LoginByEmail(team2.Name, user3.Email, user3.Password) + Client.Login(user3.Email, user3.Password) Client.SetTeamId(team2.Id) channel3 := th.CreateChannel(Client, team2) @@ -512,7 +512,7 @@ func TestSearchPostsFromUser(t *testing.T) { t.Fatalf("wrong number of posts returned %v", len(result.Order)) } - Client.LoginByEmail(team.Name, user3.Email, user3.Password) + Client.Login(user3.Email, user3.Password) // wait for the join/leave messages to be created for user3 since they're done asynchronously time.Sleep(100 * time.Millisecond) @@ -741,7 +741,7 @@ func TestGetOutOfChannelMentions(t *testing.T) { user4 := th.CreateUser(Client) LinkUserToTeam(user4, team2) - Client.Must(Client.LoginByEmail(team2.Name, user4.Email, user4.Password)) + Client.Must(Client.Login(user4.Email, user4.Password)) Client.SetTeamId(team2.Id) channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} diff --git a/api/team_test.go b/api/team_test.go index 161c7e620..a58260fd2 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -90,7 +90,7 @@ func TestCreateTeam(t *testing.T) { LinkUserToTeam(user, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(rteam.Data.(*model.Team).Id) c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) @@ -209,7 +209,7 @@ func TestAddUserToTeamFromInvite(t *testing.T) { user2 := th.CreateUser(th.BasicClient) Client.Must(Client.Logout()) - Client.Must(Client.LoginByEmail("", user2.Email, user2.Password)) + Client.Must(Client.Login(user2.Email, user2.Password)) if result, err := th.BasicClient.AddUserToTeamFromInvite("", "", rteam.InviteId); err != nil { t.Fatal(err) @@ -234,7 +234,7 @@ func TestGetAllTeams(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeams(); err != nil { @@ -254,7 +254,7 @@ func TestGetAllTeams(t *testing.T) { c.IpAddress = "cmd_line" UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeams(); err != nil { @@ -283,7 +283,7 @@ func TestGetAllTeamListings(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeamListings(); err != nil { @@ -303,7 +303,7 @@ func TestGetAllTeamListings(t *testing.T) { c.IpAddress = "cmd_line" UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if r1, err := Client.GetAllTeams(); err != nil { @@ -332,7 +332,7 @@ func TestTeamPermDelete(t *testing.T) { LinkUserToTeam(user1, team) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.Login(user1.Email, "pwd") Client.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} @@ -375,7 +375,7 @@ func TestInviteMembers(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) invite := make(map[string]string) @@ -413,7 +413,7 @@ func TestUpdateTeamDisplayName(t *testing.T) { LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") Client.SetTeamId(team.Id) vteam := &model.Team{DisplayName: team.DisplayName, Name: team.Name, Email: team.Email, Type: team.Type} @@ -422,7 +422,7 @@ func TestUpdateTeamDisplayName(t *testing.T) { t.Fatal("Should have errored, not admin") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") vteam.DisplayName = "" if _, err := Client.UpdateTeam(vteam); err == nil { @@ -474,7 +474,7 @@ func TestGetMyTeam(t *testing.T) { LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) Client.SetTeamId(team.Id) if result, err := Client.GetMyTeam(""); err != nil { diff --git a/api/user.go b/api/user.go index d8e2e6623..aee4dab61 100644 --- a/api/user.go +++ b/api/user.go @@ -45,7 +45,6 @@ func InitUser() { BaseRoutes.Users.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST") BaseRoutes.Users.Handle("/login", ApiAppHandler(login)).Methods("POST") BaseRoutes.Users.Handle("/logout", ApiAppHandler(logout)).Methods("POST") - BaseRoutes.Users.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") BaseRoutes.Users.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") BaseRoutes.Users.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") BaseRoutes.Users.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") @@ -333,7 +332,7 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service } } - Login(c, w, r, ruser, "") + doLogin(c, w, r, ruser, "") if c.Err != nil { return nil } @@ -431,114 +430,86 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, siteURL string) { func login(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - if len(props["password"]) == 0 { + id := props["id"] + loginId := props["login_id"] + password := props["password"] + mfaToken := props["token"] + deviceId := props["device_id"] + + 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 - if len(props["id"]) != 0 { - user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"]) - } else if len(props["email"]) != 0 { - user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"]) - } else if len(props["username"]) != 0 { - user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"]) - } else { - c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest - return - } + var err *model.AppError - if c.Err != nil { - return - } + if len(id) != 0 { + c.LogAuditWithUserId(id, "attempt") - if user != nil { - user.Sanitize(map[string]bool{}) - } else { - user = &model.User{} - } - w.Write([]byte(user.ToJson())) -} - -func doUserPasswordAuthenticationAndLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, password string, mfaToken string, deviceId string) bool { - c.LogAuditWithUserId(user.Id, "attempt") - if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil { - c.LogAuditWithUserId(user.Id, "fail") - c.Err = err - c.Err.StatusCode = http.StatusUnauthorized - return false - } else { - Login(c, w, r, user, deviceId) - c.LogAuditWithUserId(user.Id, "success") - return true - } -} - -func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, mfaToken, deviceId string) *model.User { - if result := <-Srv.Store.User().Get(userId); result.Err != nil { - c.Err = result.Err - return nil + if result := <-Srv.Store.User().Get(id); result.Err != nil { + c.LogAuditWithUserId(user.Id, "failure") + c.Err = result.Err + c.Err.StatusCode = http.StatusBadRequest + return + } else { + user = result.Data.(*model.User) + } } else { - user := result.Data.(*model.User) + c.LogAudit("attempt") - if len(user.AuthData) != 0 { - c.Err = model.NewLocAppError("LoginById", "api.user.login_by_email.sign_in.app_error", - map[string]interface{}{"AuthService": user.AuthService}, "") - return nil + if user, err = getUserForLogin(loginId); err != nil { + c.LogAudit("failure") + c.Err = err + return } - if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { - return user - } + c.LogAuditWithUserId(user.Id, "attempt") } - return nil -} + // and then authenticate them + if user, err = authenticateUser(user, password, mfaToken); err != nil { + c.LogAuditWithUserId(user.Id, "failure") + c.Err = err + return + } -func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User { - if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { - c.Err = result.Err - c.Err.StatusCode = http.StatusUnauthorized - return nil - } else { - user := result.Data.(*model.User) + c.LogAuditWithUserId(user.Id, "success") - if len(user.AuthData) != 0 { - c.Err = model.NewLocAppError("LoginByEmail", "api.user.login_by_email.sign_in.app_error", - map[string]interface{}{"AuthService": user.AuthService}, "") - return nil - } + doLogin(c, w, r, user, deviceId) - if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { - return user - } - } + user.Sanitize(map[string]bool{}) - return nil + w.Write([]byte(user.ToJson())) } -func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User { - if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil { - c.Err = result.Err - c.Err.StatusCode = http.StatusUnauthorized - return nil - } else { - user := result.Data.(*model.User) +func getUserForLogin(loginId string) (*model.User, *model.AppError) { + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil - if len(user.AuthData) != 0 { - c.Err = model.NewLocAppError("LoginByUsername", "api.user.login_by_email.sign_in.app_error", - map[string]interface{}{"AuthService": user.AuthService}, "") - return nil + if result := <-Srv.Store.User().GetForLogin( + loginId, + *utils.Cfg.EmailSettings.EnableSignInWithUsername, + *utils.Cfg.EmailSettings.EnableSignInWithEmail, + ldapAvailable, + ); result.Err != nil { + + if !ldapAvailable { + // failed to find user and no LDAP server to fall back on + result.Err.StatusCode = http.StatusBadRequest + return nil, result.Err } - if doUserPasswordAuthenticationAndLogin(c, w, r, user, password, mfaToken, deviceId) { - return user + // fall back to LDAP server to see if we can find a user + if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil { + ldapErr.StatusCode = http.StatusBadRequest + return nil, ldapErr + } else { + return ldapUser, nil } + } else { + return result.Data.(*model.User), nil } - - return nil } func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User { @@ -570,80 +541,13 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st return nil } else { user = result.Data.(*model.User) - Login(c, w, r, user, "") + doLogin(c, w, r, user, "") return user } } -func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { - if !*utils.Cfg.LdapSettings.Enable { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - props := model.MapFromJson(r.Body) - - password := props["password"] - id := props["id"] - mfaToken := props["token"] - - if len(password) == 0 { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest - return - } - - if len(id) == 0 { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.need_id.app_error", nil, "") - c.Err.StatusCode = http.StatusBadRequest - return - } - - ldapInterface := einterfaces.GetLdapInterface() - if ldapInterface == nil { - c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.not_available.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - user, err := ldapInterface.DoLogin(id, password) - if err != nil { - if user != nil { - c.LogAuditWithUserId(user.Id, "attempt") - c.LogAuditWithUserId(user.Id, "fail") - } else { - c.LogAudit("attempt") - c.LogAudit("fail") - } - c.Err = err - c.Err.StatusCode = http.StatusUnauthorized - return - } - c.LogAuditWithUserId(user.Id, "attempt") - - if err = checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil { - c.LogAuditWithUserId(user.Id, "fail") - c.Err = err - c.Err.StatusCode = http.StatusUnauthorized - return - } - - // User is authenticated at this point - - Login(c, w, r, user, props["device_id"]) - c.LogAuditWithUserId(user.Id, "success") - - if user != nil { - user.Sanitize(map[string]bool{}) - } else { - user = &model.User{} - } - w.Write([]byte(user.ToJson())) -} - // User MUST be authenticated completely before calling Login -func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { +func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { session := &model.Session{UserId: user.Id, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} @@ -2371,28 +2275,20 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - method := props["method"] - if method != model.USER_AUTH_SERVICE_EMAIL && - method != model.USER_AUTH_SERVICE_USERNAME && - method != model.USER_AUTH_SERVICE_LDAP { - c.SetInvalidParam("checkMfa", "method") - return - } - loginId := props["login_id"] if len(loginId) == 0 { c.SetInvalidParam("checkMfa", "login_id") return } - var uchan store.StoreChannel - if method == model.USER_AUTH_SERVICE_EMAIL { - uchan = Srv.Store.User().GetByEmail(loginId) - } else if method == model.USER_AUTH_SERVICE_USERNAME { - uchan = Srv.Store.User().GetByUsername(loginId) - } else if method == model.USER_AUTH_SERVICE_LDAP { - uchan = Srv.Store.User().GetByAuth(loginId, model.USER_AUTH_SERVICE_LDAP) - } + // we don't need to worry about contacting the ldap server to get this user because + // only users already in the system could have MFA enabled + uchan := Srv.Store.User().GetForLogin( + loginId, + *utils.Cfg.EmailSettings.EnableSignInWithUsername, + *utils.Cfg.EmailSettings.EnableSignInWithEmail, + *utils.Cfg.LdapSettings.Enable, + ) rdata := map[string]string{} if result := <-uchan; result.Err != nil { diff --git a/api/user_test.go b/api/user_test.go index 3c744120c..ee13f43f2 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -84,6 +84,19 @@ func TestLogin(t *testing.T) { th := Setup() Client := th.CreateClient() + enableSignInWithEmail := *utils.Cfg.EmailSettings.EnableSignInWithEmail + enableSignInWithUsername := *utils.Cfg.EmailSettings.EnableSignInWithUsername + enableLdap := *utils.Cfg.LdapSettings.Enable + defer func() { + *utils.Cfg.EmailSettings.EnableSignInWithEmail = enableSignInWithEmail + *utils.Cfg.EmailSettings.EnableSignInWithUsername = enableSignInWithUsername + *utils.Cfg.LdapSettings.Enable = enableLdap + }() + + *utils.Cfg.EmailSettings.EnableSignInWithEmail = false + *utils.Cfg.EmailSettings.EnableSignInWithUsername = false + *utils.Cfg.LdapSettings.Enable = false + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := Client.CreateTeam(&team) @@ -100,7 +113,12 @@ func TestLogin(t *testing.T) { } } - if result, err := Client.LoginByEmail(team.Name, user.Email, user.Password); err != nil { + if _, err := Client.Login(user.Email, user.Password); err == nil { + t.Fatal("shouldn't be able to log in by email when disabled") + } + + *utils.Cfg.EmailSettings.EnableSignInWithEmail = true + if result, err := Client.Login(user.Email, user.Password); err != nil { t.Fatal(err) } else { if result.Data.(*model.User).Email != user.Email { @@ -108,7 +126,12 @@ func TestLogin(t *testing.T) { } } - if result, err := Client.LoginByUsername(team.Name, user.Username, user.Password); err != nil { + if _, err := Client.Login(user.Username, user.Password); err == nil { + t.Fatal("shouldn't be able to log in by username when disabled") + } + + *utils.Cfg.EmailSettings.EnableSignInWithUsername = true + if result, err := Client.Login(user.Username, user.Password); err != nil { t.Fatal(err) } else { if result.Data.(*model.User).Email != user.Email { @@ -116,19 +139,19 @@ func TestLogin(t *testing.T) { } } - if _, err := Client.LoginByEmail(team.Name, user.Email, user.Password+"invalid"); err == nil { + if _, err := Client.Login(user.Email, user.Password+"invalid"); err == nil { t.Fatal("Invalid Password") } - if _, err := Client.LoginByUsername(team.Name, user.Username, user.Password+"invalid"); err == nil { + if _, err := Client.Login(user.Username, user.Password+"invalid"); err == nil { t.Fatal("Invalid Password") } - if _, err := Client.LoginByEmail(team.Name, "", user.Password); err == nil { + if _, err := Client.Login("", user.Password); err == nil { t.Fatal("should have failed") } - if _, err := Client.LoginByUsername(team.Name, "", user.Password); err == nil { + if _, err := Client.Login("", user.Password); err == nil { t.Fatal("should have failed") } @@ -160,22 +183,35 @@ func TestLogin(t *testing.T) { ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash) - if _, err := Client.LoginByEmail(team2.Name, ruser2.Data.(*model.User).Email, user2.Password); err != nil { + if _, err := Client.Login(ruser2.Data.(*model.User).Email, user2.Password); err != nil { t.Fatal("From verfied hash") } Client.AuthToken = authToken + + user3 := &model.User{ + Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", + Nickname: "Corey Hulen", + Username: "corey" + model.NewId(), + Password: "pwd", + AuthService: model.USER_AUTH_SERVICE_LDAP, + } + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user3.Id)) + + if _, err := Client.Login(user3.Id, user3.Password); err == nil { + t.Fatal("LDAP user should not be able to log in with LDAP disabled") + } } func TestLoginWithDeviceId(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient - team := th.BasicTeam user := th.BasicUser Client.Must(Client.Logout()) deviceId := model.NewId() - if result, err := Client.LoginByEmailWithDevice(team.Name, user.Email, user.Password, deviceId); err != nil { + if result, err := Client.LoginWithDevice(user.Email, user.Password, deviceId); err != nil { t.Fatal(err) } else { ruser := result.Data.(*model.User) @@ -184,7 +220,7 @@ func TestLoginWithDeviceId(t *testing.T) { t.Fatal(err) } else { sessions := ssresult.Data.([]*model.Session) - if _, err := Client.LoginByEmailWithDevice(team.Name, user.Email, user.Password, deviceId); err != nil { + if _, err := Client.LoginWithDevice(user.Email, user.Password, deviceId); err != nil { t.Fatal(err) } @@ -198,13 +234,12 @@ func TestLoginWithDeviceId(t *testing.T) { func TestSessions(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient - team := th.BasicTeam user := th.BasicUser Client.Must(Client.Logout()) deviceId := model.NewId() - Client.LoginByEmailWithDevice(team.Name, user.Email, user.Password, deviceId) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.LoginWithDevice(user.Email, user.Password, deviceId) + Client.Login(user.Email, user.Password) r1, err := Client.GetSessions(user.Id) if err != nil { @@ -269,7 +304,7 @@ func TestGetUser(t *testing.T) { LinkUserToTeam(ruser3.Data.(*model.User), rteam2.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id)) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) rId := ruser.Data.(*model.User).Id if result, err := Client.GetUser(rId, ""); err != nil { @@ -333,7 +368,7 @@ func TestGetUser(t *testing.T) { c.IpAddress = "cmd_line" UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil { t.Fatal(err) @@ -374,7 +409,7 @@ func TestGetAudits(t *testing.T) { time.Sleep(100 * time.Millisecond) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) time.Sleep(100 * time.Millisecond) @@ -427,7 +462,7 @@ func TestUserCreateImage(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.DoApiGet("/users/"+user.Id+"/image", "", "") @@ -471,7 +506,7 @@ func TestUserUploadProfileImage(t *testing.T) { t.Fatal("Should have errored") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if _, upErr := Client.UploadProfileFile(body.Bytes(), writer.FormDataContentType()); upErr == nil { @@ -575,7 +610,7 @@ func TestUserUpdate(t *testing.T) { t.Fatal("Should have errored") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) time.Sleep(100 * time.Millisecond) @@ -615,7 +650,7 @@ func TestUserUpdate(t *testing.T) { LinkUserToTeam(user2, team) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") Client.SetTeamId(team.Id) user.Nickname = "Tim Timmy" @@ -642,7 +677,7 @@ func TestUserUpdatePassword(t *testing.T) { t.Fatal("Should have errored") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") if _, err := Client.UpdateUserPassword("123", "pwd", "newpwd"); err == nil { t.Fatal("Should have errored") @@ -673,7 +708,7 @@ func TestUserUpdatePassword(t *testing.T) { t.Fatal("LastPasswordUpdate should have changed") } - if _, err := Client.LoginByEmail(team.Name, user.Email, "newpwd"); err != nil { + if _, err := Client.Login(user.Email, "newpwd"); err != nil { t.Fatal(err) } @@ -681,7 +716,7 @@ func TestUserUpdatePassword(t *testing.T) { user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) LinkUserToTeam(user2, team) - Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") if _, err := Client.UpdateUserPassword(user.Id, "pwd", "newpwd"); err == nil { t.Fatal("Should have errored") @@ -713,7 +748,7 @@ func TestUserUpdateRoles(t *testing.T) { t.Fatal("Should have errored, not logged in") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") Client.SetTeamId(team.Id) if _, err := Client.UpdateUserRoles(data); err == nil { @@ -728,7 +763,7 @@ func TestUserUpdateRoles(t *testing.T) { LinkUserToTeam(user3, team2) store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - Client.LoginByEmail(team2.Name, user3.Email, "pwd") + Client.Login(user3.Email, "pwd") Client.SetTeamId(team2.Id) data["user_id"] = user2.Id @@ -737,7 +772,7 @@ func TestUserUpdateRoles(t *testing.T) { t.Fatal("Should have errored, wrong team") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") data["user_id"] = "junk" data["new_roles"] = "admin" @@ -771,7 +806,7 @@ func TestUserUpdateDeviceId(t *testing.T) { LinkUserToTeam(user, team) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) deviceId := model.PUSH_NOTIFY_APPLE + ":1234567890" @@ -811,7 +846,7 @@ func TestUserUpdateActive(t *testing.T) { t.Fatal("Should have errored, not logged in") } - Client.LoginByEmail(team.Name, user2.Email, "pwd") + Client.Login(user2.Email, "pwd") Client.SetTeamId(team.Id) if _, err := Client.UpdateActive(user.Id, false); err == nil { @@ -828,14 +863,14 @@ func TestUserUpdateActive(t *testing.T) { LinkUserToTeam(user2, team2) store.Must(Srv.Store.User().VerifyEmail(user3.Id)) - Client.LoginByEmail(team2.Name, user3.Email, "pwd") + Client.Login(user3.Email, "pwd") Client.SetTeamId(team2.Id) if _, err := Client.UpdateActive(user.Id, false); err == nil { t.Fatal("Should have errored, not yourself") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if _, err := Client.UpdateActive("junk", false); err == nil { @@ -859,7 +894,7 @@ func TestUserPermDelete(t *testing.T) { LinkUserToTeam(user1, team) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - Client.LoginByEmail(team.Name, user1.Email, "pwd") + Client.Login(user1.Email, "pwd") Client.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} @@ -1013,7 +1048,7 @@ func TestUserUpdateNotify(t *testing.T) { t.Fatal("Should have errored - not logged in") } - Client.LoginByEmail(team.Name, user.Email, "pwd") + Client.Login(user.Email, "pwd") Client.SetTeamId(team.Id) if result, err := Client.UpdateUserNotify(data); err != nil { @@ -1109,7 +1144,7 @@ func TestStatuses(t *testing.T) { LinkUserToTeam(ruser2, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser2.Id)) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) Client.SetTeamId(team.Id) userIds := []string{ruser2.Id} @@ -1207,7 +1242,7 @@ func TestOAuthToEmail(t *testing.T) { t.Fatal("should have failed - not logged in") } - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) if _, err := Client.OAuthToEmail(m); err == nil { t.Fatal("should have failed - empty data") @@ -1248,7 +1283,7 @@ func TestLDAPToEmail(t *testing.T) { LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) m := map[string]string{} if _, err := Client.LDAPToEmail(m); err == nil { @@ -1301,7 +1336,7 @@ func TestEmailToLDAP(t *testing.T) { LinkUserToTeam(ruser, rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) m := map[string]string{} if _, err := Client.EmailToLDAP(m); err == nil { @@ -1438,7 +1473,7 @@ func TestGenerateMfaQrCode(t *testing.T) { t.Fatal("should have failed - not logged in") } - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) if _, err := Client.GenerateMfaQrCode(); err == nil { t.Fatal("should have failed - not licensed") @@ -1476,7 +1511,7 @@ func TestUpdateMfa(t *testing.T) { t.Fatal("should have failed - not logged in") } - Client.LoginByEmail(team.Name, user.Email, user.Password) + Client.Login(user.Email, user.Password) if _, err := Client.UpdateMfa(true, ""); err == nil { t.Fatal("should have failed - no token") @@ -1509,7 +1544,7 @@ func TestCheckMfa(t *testing.T) { LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team)) store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) - if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, user.Email); err != nil { + if result, err := Client.CheckMfa(user.Email); err != nil { t.Fatal(err) } else { resp := result.Data.(map[string]string) diff --git a/i18n/en.json b/i18n/en.json index 138a1ace1..9a44ca31a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1560,7 +1560,7 @@ "translation": "Revoking sessionId=%v for userId=%v re-login with same device Id" }, { - "id": "api.user.login_by_email.sign_in.app_error", + "id": "api.user.login.use_auth_service.app_error", "translation": "Please sign in using {{.AuthService}}" }, { @@ -3444,6 +3444,14 @@ "translation": "We encountered an error while finding user profiles" }, { + "id": "store.sql_user.get_for_login.app_error", + "translation": "We couldn't find an existing account matching your credentials. This team may require an invite from the team owner to join." + }, + { + "id": "store.sql_user.get_for_login.multiple_users", + "translation": "We found multiple users matching your credentials and were unable to log you in. Please contact an administrator." + }, + { "id": "store.sql_user.get_profiles.app_error", "translation": "We encountered an error while finding user profiles" }, diff --git a/i18n/es.json b/i18n/es.json index 5bda4b981..f83a2591c 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -1560,7 +1560,7 @@ "translation": "Revocando sessionId=%v para el userId=%v vuelve a iniciar la sesión el el mismo dispositivo" }, { - "id": "api.user.login_by_email.sign_in.app_error", + "id": "api.user.login.use_auth_service.app_error", "translation": "Por favor inicia sesión usando {{.AuthService}}" }, { diff --git a/i18n/fr.json b/i18n/fr.json index 488b49b69..d2290685c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1428,7 +1428,7 @@ "translation": "Session sessionId=%v de l'utilisateur userId=%v révoquée re-connexion avec le même identifiant" }, { - "id": "api.user.login_by_email.sign_in.app_error", + "id": "api.user.login.use_auth_service.app_error", "translation": "Veuillez vous connecter en utilisant {{.AuthService}}" }, { diff --git a/i18n/ja.json b/i18n/ja.json index 44b8781f0..652494b67 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -1564,7 +1564,7 @@ "translation": "userId=%vのsessionId=%vを無効化しました。同じデバイスIDでログインし直してください" }, { - "id": "api.user.login_by_email.sign_in.app_error", + "id": "api.user.login.use_auth_service.app_error", "translation": "{{.AuthService}}を使ってサインインしてください" }, { diff --git a/i18n/pt.json b/i18n/pt.json index 635e254d1..2f4b2dd37 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -1520,7 +1520,7 @@ "translation": "Revogada sessionid=%v para userid=%v logue novamente com o mesmo id de dispositivo" }, { - "id": "api.user.login_by_email.sign_in.app_error", + "id": "api.user.login.use_auth_service.app_error", "translation": "Por favor logue usando {{.AuthService}}" }, { diff --git a/model/client.go b/model/client.go index e9f22e452..7eab008f1 100644 --- a/model/client.go +++ b/model/client.go @@ -355,51 +355,21 @@ func (c *Client) LoginById(id string, password string) (*Result, *AppError) { return c.login(m) } -func (c *Client) LoginByEmail(name string, email string, password string) (*Result, *AppError) { +func (c *Client) Login(loginId string, password string) (*Result, *AppError) { m := make(map[string]string) - m["name"] = name - m["email"] = email - m["password"] = password - return c.login(m) -} - -func (c *Client) LoginByUsername(name string, username string, password string) (*Result, *AppError) { - m := make(map[string]string) - m["name"] = name - m["username"] = username + m["login_id"] = loginId m["password"] = password return c.login(m) } -func (c *Client) LoginByEmailWithDevice(name string, email string, password string, deviceId string) (*Result, *AppError) { +func (c *Client) LoginWithDevice(loginId string, password string, deviceId string) (*Result, *AppError) { m := make(map[string]string) - m["name"] = name - m["email"] = email + m["login_id"] = loginId m["password"] = password m["device_id"] = deviceId return c.login(m) } -func (c *Client) LoginByLdap(userid string, password string, mfatoken string) (*Result, *AppError) { - m := make(map[string]string) - m["id"] = userid - m["password"] = password - m["token"] = mfatoken - if r, err := c.DoApiPost("/users/login_ldap", MapToJson(m)); err != nil { - return nil, err - } else { - c.AuthToken = r.Header.Get(HEADER_TOKEN) - c.AuthType = HEADER_BEARER - sessionToken := getCookie(SESSION_COOKIE_TOKEN, r) - - if c.AuthToken != sessionToken.Value { - NewLocAppError("/users/login_ldap", "model.client.login.app_error", nil, "") - } - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - func (c *Client) login(m map[string]string) (*Result, *AppError) { if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { return nil, err @@ -430,9 +400,8 @@ func (c *Client) Logout() (*Result, *AppError) { } } -func (c *Client) CheckMfa(method, loginId string) (*Result, *AppError) { +func (c *Client) CheckMfa(loginId string) (*Result, *AppError) { m := make(map[string]string) - m["method"] = method m["login_id"] = loginId if r, err := c.DoApiPost("/users/mfa", MapToJson(m)); err != nil { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 4d18aabd1..37bc148e1 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -716,6 +716,47 @@ func (us SqlUserStore) GetByUsername(username string) StoreChannel { return storeChannel } +func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + params := map[string]interface{}{ + "LoginId": loginId, + "AllowSignInWithUsername": allowSignInWithUsername, + "AllowSignInWithEmail": allowSignInWithEmail, + "LdapEnabled": ldapEnabled, + } + + users := []*model.User{} + if _, err := us.GetReplica().Select( + &users, + `SELECT + * + FROM + Users + WHERE + (:AllowSignInWithUsername AND Username = :LoginId) + OR (:AllowSignInWithEmail AND Email = :LoginId) + OR (:LdapEnabled AND AuthService = '`+model.USER_AUTH_SERVICE_LDAP+`' AND AuthData = :LoginId)`, + params); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetForLogin", "store.sql_user.get_for_login.app_error", nil, err.Error()) + } else if len(users) == 1 { + result.Data = users[0] + } else if len(users) > 1 { + result.Err = model.NewLocAppError("SqlUserStore.GetForLogin", "store.sql_user.get_for_login.multiple_users", nil, "") + } else { + result.Err = model.NewLocAppError("SqlUserStore.GetForLogin", "store.sql_user.get_for_login.app_error", nil, "") + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) VerifyEmail(userId string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 9fed32dc8..fbe2285a9 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -469,6 +469,82 @@ func TestUserStoreGetByUsername(t *testing.T) { } } +func TestUserStoreGetForLogin(t *testing.T) { + Setup() + + u1 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + AuthService: model.USER_AUTH_SERVICE_GITLAB, + AuthData: model.NewId(), + } + Must(store.User().Save(u1)) + + u2 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + AuthService: model.USER_AUTH_SERVICE_LDAP, + AuthData: model.NewId(), + } + Must(store.User().Save(u2)) + + if result := <-store.User().GetForLogin(u1.Username, true, true, true); result.Err != nil { + t.Fatal("Should have gotten user by username", result.Err) + } else if result.Data.(*model.User).Id != u1.Id { + t.Fatal("Should have gotten user1 by username") + } + + if result := <-store.User().GetForLogin(u1.Email, true, true, true); result.Err != nil { + t.Fatal("Should have gotten user by email", result.Err) + } else if result.Data.(*model.User).Id != u1.Id { + t.Fatal("Should have gotten user1 by email") + } + + if result := <-store.User().GetForLogin(u2.AuthData, true, true, true); result.Err != nil { + t.Fatal("Should have gotten user by LDAP AuthData", result.Err) + } else if result.Data.(*model.User).Id != u2.Id { + t.Fatal("Should have gotten user2 by LDAP AuthData") + } + + // prevent getting user by AuthData when they're not an LDAP user + if result := <-store.User().GetForLogin(u1.AuthData, true, true, true); result.Err == nil { + t.Fatal("Should not have gotten user by non-LDAP AuthData") + } + + // prevent getting user when different login methods are disabled + if result := <-store.User().GetForLogin(u1.Username, false, true, true); result.Err == nil { + t.Fatal("Should have failed to get user1 by username") + } + + if result := <-store.User().GetForLogin(u1.Email, true, false, true); result.Err == nil { + t.Fatal("Should have failed to get user1 by email") + } + + if result := <-store.User().GetForLogin(u2.AuthData, true, true, false); result.Err == nil { + t.Fatal("Should have failed to get user3 by LDAP AuthData") + } + + // test a special case where two users will have conflicting login information so we throw a special error + u3 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + AuthService: model.USER_AUTH_SERVICE_LDAP, + AuthData: model.NewId(), + } + Must(store.User().Save(u3)) + u4 := &model.User{ + Email: model.NewId(), + Username: model.NewId(), + AuthService: model.USER_AUTH_SERVICE_LDAP, + AuthData: u3.Username, + } + Must(store.User().Save(u4)) + + if err := (<-store.User().GetForLogin(u3.Username, true, true, true)).Err; err == nil { + t.Fatal("Should have failed to get users with conflicting login information") + } +} + func TestUserStoreUpdatePassword(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index f5c440721..8097fd8e8 100644 --- a/store/store.go +++ b/store/store.go @@ -137,6 +137,7 @@ type UserStore interface { GetByEmail(email string) StoreChannel GetByAuth(authData string, authService string) StoreChannel GetByUsername(username string) StoreChannel + GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel VerifyEmail(userId string) StoreChannel GetEtagForProfiles(teamId string) StoreChannel GetEtagForDirectProfiles(userId string) StoreChannel diff --git a/web/web_test.go b/web/web_test.go index 9d7a84c5c..7a0485215 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -204,7 +204,7 @@ func TestIncomingWebhook(t *testing.T) { c.RequestId = model.NewId() c.IpAddress = "cmd_line" api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) - ApiClient.LoginByEmail(team.Name, user.Email, "pwd") + ApiClient.Login(user.Email, "pwd") ApiClient.SetTeamId(team.Id) channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 6a7c5de40..69f573eff 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -746,49 +746,30 @@ export default class Client { end(this.handleResponse.bind(this, 'getMe', success, error)); } - login = (email, username, password, mfaToken, success, error) => { - var outer = this; // eslint-disable-line consistent-this + login = (loginId, password, mfaToken, success, error) => { + this.doLogin({login_id: loginId, password, token: mfaToken}, success, error); - request. - post(`${this.getUsersRoute()}/login`). - set(this.defaultHeaders). - type('application/json'). - accept('application/json'). - send({email, password, username, token: mfaToken}). - end(this.handleResponse.bind( - this, - 'login', - (data, res) => { - if (res && res.header) { - outer.token = res.header[HEADER_TOKEN]; - - if (outer.useToken) { - outer.defaultHeaders[HEADER_AUTH] = `${HEADER_BEARER} ${outer.token}`; - } - } + this.track('api', 'api_users_login', '', 'login_id', loginId); + } - if (success) { - success(data, res); - } - }, - error - )); + loginById = (id, password, mfaToken, success, error) => { + this.doLogin({id, password, token: mfaToken}, success, error); - this.track('api', 'api_users_login', '', 'email', email); + this.track('api', 'api_users_login', '', 'id', id); } - loginByLdap = (ldapId, password, mfaToken, success, error) => { + doLogin = (outgoingData, success, error) => { var outer = this; // eslint-disable-line consistent-this request. - post(`${this.getUsersRoute()}/login_ldap`). + post(`${this.getUsersRoute()}/login`). set(this.defaultHeaders). type('application/json'). accept('application/json'). - send({id: ldapId, password, token: mfaToken}). + send(outgoingData). end(this.handleResponse.bind( this, - 'loginByLdap', + 'login', (data, res) => { if (res && res.header) { outer.token = res.header[HEADER_TOKEN]; @@ -804,8 +785,6 @@ export default class Client { }, error )); - - this.track('api', 'api_users_loginLdap', '', 'email', ldapId); } logout = (success, error) => { @@ -819,10 +798,10 @@ export default class Client { this.track('api', 'api_users_logout'); } - checkMfa = (method, loginId, success, error) => { - var data = {}; - data.method = method; - data.login_id = loginId; + checkMfa = (loginId, success, error) => { + const data = { + login_id: loginId + }; request. post(`${this.getUsersRoute()}/mfa`). diff --git a/webapp/components/login/components/login_email.jsx b/webapp/components/login/components/login_email.jsx deleted file mode 100644 index b1f484c08..000000000 --- a/webapp/components/login/components/login_email.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Utils from 'utils/utils.jsx'; -import UserStore from 'stores/user_store.jsx'; -import Constants from 'utils/constants.jsx'; - -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; - -export default class LoginEmail extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.state = { - serverError: props.serverError - }; - } - componentWillReceiveProps(nextProps) { - this.setState({serverError: nextProps.serverError}); - } - handleSubmit(e) { - e.preventDefault(); - var state = {}; - - const email = this.refs.email.value.trim(); - if (!email) { - state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required'); - this.setState(state); - return; - } - - const password = this.refs.password.value.trim(); - if (!password) { - state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required'); - this.setState(state); - return; - } - - state.serverError = ''; - this.setState(state); - - this.props.submit(Constants.EMAIL_SERVICE, email, password); - } - render() { - let serverError; - let errorClass = ''; - if (this.state.serverError) { - serverError = <label className='control-label'>{this.state.serverError}</label>; - errorClass = ' has-error'; - } - - let priorEmail = UserStore.getLastEmail(); - let focusEmail = false; - let focusPassword = false; - if (priorEmail === '') { - focusEmail = true; - } else { - focusPassword = true; - } - - const emailParam = Utils.getUrlParameter('email'); - if (emailParam) { - priorEmail = decodeURIComponent(emailParam); - } - - return ( - <form onSubmit={this.handleSubmit}> - <div className='signup__email-container'> - <div className={'form-group' + errorClass}> - {serverError} - </div> - <div className={'form-group' + errorClass}> - <input - autoFocus={focusEmail} - type='email' - className='form-control' - name='email' - defaultValue={priorEmail} - ref='email' - placeholder={Utils.localizeMessage('login_email.email', 'Email')} - spellCheck='false' - /> - </div> - <div className={'form-group' + errorClass}> - <input - autoFocus={focusPassword} - type='password' - className='form-control' - name='password' - ref='password' - placeholder={Utils.localizeMessage('login_email.pwd', 'Password')} - spellCheck='false' - /> - </div> - <div className='form-group'> - <button - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='login_email.signin' - defaultMessage='Sign in' - /> - </button> - </div> - </div> - </form> - ); - } -} -LoginEmail.defaultProps = { -}; - -LoginEmail.propTypes = { - submit: React.PropTypes.func.isRequired, - serverError: React.PropTypes.string -}; diff --git a/webapp/components/login/components/login_ldap.jsx b/webapp/components/login/components/login_ldap.jsx deleted file mode 100644 index 36b8a406c..000000000 --- a/webapp/components/login/components/login_ldap.jsx +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Utils from 'utils/utils.jsx'; -import Constants from 'utils/constants.jsx'; - -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; - -export default class LoginLdap extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.state = { - serverError: props.serverError - }; - } - componentWillReceiveProps(nextProps) { - this.setState({serverError: nextProps.serverError}); - } - handleSubmit(e) { - e.preventDefault(); - const state = {}; - - const id = this.refs.id.value.trim(); - if (!id) { - state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required'); - this.setState(state); - return; - } - - const password = this.refs.password.value.trim(); - if (!password) { - state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required'); - this.setState(state); - return; - } - - state.serverError = ''; - this.setState(state); - - this.props.submit(Constants.LDAP_SERVICE, id, password); - } - render() { - let serverError; - let errorClass = ''; - if (this.state.serverError) { - serverError = <label className='control-label'>{this.state.serverError}</label>; - errorClass = ' has-error'; - } - - let loginPlaceholder; - if (global.window.mm_config.LdapLoginFieldName) { - loginPlaceholder = global.window.mm_config.LdapLoginFieldName; - } else { - loginPlaceholder = Utils.localizeMessage('login_ldap.username', 'LDAP Username'); - } - - let passwordPlaceholder; - if (global.window.mm_config.LdapPasswordFieldName) { - passwordPlaceholder = global.window.mm_config.LdapPasswordFieldName; - } else { - passwordPlaceholder = Utils.localizeMessage('login_ldap.pwd', 'LDAP Password'); - } - - return ( - <form onSubmit={this.handleSubmit}> - <div className='signup__email-container'> - <div className={'form-group' + errorClass}> - {serverError} - </div> - <div className={'form-group' + errorClass}> - <input - autoFocus={true} - className='form-control' - ref='id' - placeholder={loginPlaceholder} - spellCheck='false' - /> - </div> - <div className={'form-group' + errorClass}> - <input - type='password' - className='form-control' - ref='password' - placeholder={passwordPlaceholder} - spellCheck='false' - /> - </div> - <div className='form-group'> - <button - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='login_ldap.signin' - defaultMessage='Sign in' - /> - </button> - </div> - </div> - </form> - ); - } -} -LoginLdap.defaultProps = { -}; - -LoginLdap.propTypes = { - serverError: React.PropTypes.string, - submit: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/login/components/login_mfa.jsx b/webapp/components/login/components/login_mfa.jsx index f8ebf1e82..f8d42012c 100644 --- a/webapp/components/login/components/login_mfa.jsx +++ b/webapp/components/login/components/login_mfa.jsx @@ -31,7 +31,7 @@ export default class LoginMfa extends React.Component { state.serverError = ''; this.setState(state); - this.props.submit(this.props.method, this.props.loginId, this.props.password, token); + this.props.submit(this.props.loginId, this.props.password, token); } render() { let serverError; @@ -85,7 +85,6 @@ LoginMfa.defaultProps = { }; LoginMfa.propTypes = { - method: React.PropTypes.string.isRequired, loginId: React.PropTypes.string.isRequired, password: React.PropTypes.string.isRequired, submit: React.PropTypes.func.isRequired diff --git a/webapp/components/login/components/login_username.jsx b/webapp/components/login/components/login_username.jsx deleted file mode 100644 index 3cb213994..000000000 --- a/webapp/components/login/components/login_username.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Utils from 'utils/utils.jsx'; -import UserStore from 'stores/user_store.jsx'; -import Constants from 'utils/constants.jsx'; - -import {FormattedMessage} from 'react-intl'; - -import React from 'react'; - -export default class LoginUsername extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.state = { - serverError: props.serverError - }; - } - componentWillReceiveProps(nextProps) { - this.setState({serverError: nextProps.serverError}); - } - handleSubmit(e) { - e.preventDefault(); - const state = {}; - - const username = this.refs.username.value.trim(); - if (!username) { - state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required'); - this.setState(state); - return; - } - - const password = this.refs.password.value.trim(); - if (!password) { - state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required'); - this.setState(state); - return; - } - - state.serverError = ''; - this.setState(state); - - this.props.submit(Constants.USERNAME_SERVICE, username, password); - } - render() { - let serverError; - let errorClass = ''; - if (this.state.serverError) { - serverError = <label className='control-label'>{this.state.serverError}</label>; - errorClass = ' has-error'; - } - - let priorUsername = UserStore.getLastUsername(); - let focusUsername = false; - let focusPassword = false; - if (priorUsername === '') { - focusUsername = true; - } else { - focusPassword = true; - } - - const emailParam = Utils.getUrlParameter('email'); - if (emailParam) { - priorUsername = decodeURIComponent(emailParam); - } - - return ( - <form onSubmit={this.handleSubmit}> - <div className='signup__email-container'> - <div className={'form-group' + errorClass}> - {serverError} - </div> - <div className={'form-group' + errorClass}> - <input - autoFocus={focusUsername} - type='username' - className='form-control' - name='username' - defaultValue={priorUsername} - ref='username' - placeholder={Utils.localizeMessage('login_username.username', 'Username')} - spellCheck='false' - /> - </div> - <div className={'form-group' + errorClass}> - <input - autoFocus={focusPassword} - type='password' - className='form-control' - name='password' - ref='password' - placeholder={Utils.localizeMessage('login_username.pwd', 'Password')} - spellCheck='false' - /> - </div> - <div className='form-group'> - <button - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='login_username.signin' - defaultMessage='Sign in' - /> - </button> - </div> - </div> - </form> - ); - } -} -LoginUsername.defaultProps = { -}; - -LoginUsername.propTypes = { - serverError: React.PropTypes.string, - submit: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login.jsx index f6c02f6a3..64ef72995 100644 --- a/webapp/components/login/login.jsx +++ b/webapp/components/login/login.jsx @@ -1,13 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import LoginEmail from './components/login_email.jsx'; -import LoginUsername from './components/login_username.jsx'; -import LoginLdap from './components/login_ldap.jsx'; import LoginMfa from './components/login_mfa.jsx'; import ErrorBar from 'components/error_bar.jsx'; +import FormError from 'components/form_error.jsx'; -import * as GlobalActions from '../../action_creators/global_actions.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import UserStore from 'stores/user_store.jsx'; import Client from 'utils/web_client.jsx'; @@ -30,10 +28,16 @@ export default class Login extends React.Component { this.submit = this.submit.bind(this); this.finishSignin = this.finishSignin.bind(this); - const state = {}; - state.showMfa = false; - this.state = state; + this.handleLoginIdChange = this.handleLoginIdChange.bind(this); + this.handlePasswordChange = this.handlePasswordChange.bind(this); + + this.state = { + loginId: '', // the browser will set a default for this + password: '', + showMfa: false + }; } + componentDidMount() { document.title = global.window.mm_config.SiteName; @@ -41,31 +45,97 @@ export default class Login extends React.Component { browserHistory.push('/select_team'); } } - preSubmit(method, loginId, password) { + + preSubmit(e) { + e.preventDefault(); + + const loginId = this.state.loginId.trim(); + if (!loginId) { + const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; + const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; + const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true'; + + this.setState({ + serverError: ( + <FormattedMessage + id='login.loginIdRequired' + defaultMessage='A {type} is required' + values={{ + type: this.createLoginPlaceholder(emailSigninEnabled, usernameSigninEnabled, ldapEnabled) + }} + /> + ) + }); + + return; + } + + const password = this.state.password.trim(); + if (!password) { + this.setState({ + serverError: ( + <FormattedMessage + id='login.passwordRequired' + defaultMessage='A password is required' + /> + ) + }); + + return; + } + if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') { - this.submit(method, loginId, password, ''); + this.submit(loginId, password, ''); return; } - Client.checkMfa(method, loginId, + Client.checkMfa( + loginId, (data) => { if (data.mfa_required === 'true') { - this.setState({showMfa: true, method, loginId, password}); + this.setState({showMfa: true}); } else { - this.submit(method, loginId, password, ''); + this.submit(loginId, password, ''); } }, (err) => { - if (method === Constants.EMAIL_SERVICE) { - this.setState({serverEmailError: err.message}); - } else if (method === Constants.USERNAME_SERVICE) { - this.setState({serverUsernameError: err.message}); - } else if (method === Constants.LDAP_SERVICE) { - this.setState({serverLdapError: err.message}); + this.setState({serverError: err.message}); + } + ); + } + + submit(loginId, password, token) { + this.setState({showMfa: false, serverError: null}); + + Client.webLogin( + loginId, + password, + token, + () => { + this.finishSignin(); + }, + (err) => { + if (err.id === 'api.user.login.not_verified.app_error') { + browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId)); + return; + } else if (err.id === 'store.sql_user.get_for_login.app_error' || + err.id === 'ent.ldap.do_login.user_not_registered.app_error' || + err.id === 'ent.ldap.do_login.user_filtered.app_error') { + this.setState({ + serverError: ( + <FormattedMessage + id='login.userNotFound' + defaultMessage="We couldn't find an existing account matching your login credentials." + /> + ) + }); + } else { + this.setState({serverError: err.message}); } } ); } + finishSignin() { GlobalActions.emitInitialLoad( () => { @@ -74,72 +144,18 @@ export default class Login extends React.Component { ); } - submit(method, loginId, password, token) { - this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null}); - - if (method === Constants.EMAIL_SERVICE) { - Client.webLogin( - loginId, - null, - password, - token, - () => { - UserStore.setLastEmail(loginId); - this.finishSignin(); - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId)); - return; - } - this.setState({serverEmailError: err.message}); - } - ); - } else if (method === Constants.USERNAME_SERVICE) { - Client.webLogin( - null, - loginId, - password, - token, - () => { - UserStore.setLastUsername(loginId); - - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - browserHistory.push(decodeURIComponent(redirect)); - } else { - this.finishSignin(); - } - }, - (err) => { - if (err.id === 'api.user.login.not_verified.app_error') { - this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')}); - } else if (err.id === 'store.sql_user.get_by_username.app_error') { - this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')}); - } else { - this.setState({serverUsernameError: err.message}); - } - } - ); - } else if (method === Constants.LDAP_SERVICE) { - Client.loginByLdap( - loginId, - password, - token, - () => { - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - browserHistory.push(decodeURIComponent(redirect)); - } else { - this.finishSignin(); - } - }, - (err) => { - this.setState({serverLdapError: err.message}); - } - ); - } + handleLoginIdChange(e) { + this.setState({ + loginId: e.target.value + }); + } + + handlePasswordChange(e) { + this.setState({ + password: e.target.value + }); } + createCustomLogin() { if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true' && @@ -158,6 +174,36 @@ export default class Login extends React.Component { return null; } + + createLoginPlaceholder(emailSigninEnabled, usernameSigninEnabled, ldapEnabled) { + const loginPlaceholders = []; + if (emailSigninEnabled) { + loginPlaceholders.push(Utils.localizeMessage('login.email', 'Email')); + } + + if (usernameSigninEnabled) { + loginPlaceholders.push(Utils.localizeMessage('login.username', 'Username')); + } + + if (ldapEnabled) { + if (global.window.mm_config.LdapLoginFieldName) { + loginPlaceholders.push(global.window.mm_config.LdapLoginFieldName); + } else { + loginPlaceholders.push(Utils.localizeMessage('login.ldap_username', 'LDAP Username')); + } + } + + if (loginPlaceholders.length >= 2) { + return loginPlaceholders.slice(0, loginPlaceholders.length - 1).join(', ') + + Utils.localizeMessage('login.placeholderOr', ' or ') + + loginPlaceholders[loginPlaceholders.length - 1]; + } else if (loginPlaceholders.length === 1) { + return loginPlaceholders[0]; + } + + return ''; + } + createLoginOptions() { const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; @@ -248,76 +294,52 @@ export default class Login extends React.Component { ); } - let emailLogin; - if (emailSigninEnabled) { - emailLogin = ( - <LoginEmail - serverError={this.state.serverEmailError} - submit={this.preSubmit} - /> - ); - - if (oauthLogins.length > 0) { - emailLogin = ( - <div> - <div className='or__container'> - <FormattedMessage - id='login.or' - defaultMessage='or' - /> - </div> - {emailLogin} - </div> - ); + let login = null; + if (emailSigninEnabled || usernameSigninEnabled || ldapEnabled) { + let errorClass = ''; + if (this.state.serverError) { + errorClass = ' has-error'; } - } - - let usernameLogin; - if (usernameSigninEnabled) { - usernameLogin = ( - <LoginUsername - serverError={this.state.serverUsernameError} - submit={this.preSubmit} - /> - ); - if (emailSigninEnabled || oauthLogins.length > 0) { - usernameLogin = ( - <div> - <div className='or__container'> - <FormattedMessage - id='login.or' - defaultMessage='or' + login = ( + <form onSubmit={this.preSubmit}> + <div className='signup__email-container'> + <FormError error={this.state.serverError}/> + <div className={'form-group' + errorClass}> + <input + className='form-control' + name='loginId' + value={this.state.loginId} + onChange={this.handleLoginIdChange} + placeholder={this.createLoginPlaceholder(emailSigninEnabled, usernameSigninEnabled, ldapEnabled)} + spellCheck='false' /> </div> - {usernameLogin} - </div> - ); - } - } - - let ldapLogin; - if (ldapEnabled) { - ldapLogin = ( - <LoginLdap - serverError={this.state.serverLdapError} - submit={this.preSubmit} - /> - ); - - if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) { - ldapLogin = ( - <div> - <div className='or__container'> - <FormattedMessage - id='login.or' - defaultMessage='or' + <div className={'form-group' + errorClass}> + <input + type='password' + className='form-control' + name='password' + value={this.state.password} + onChange={this.handlePasswordChange} + placeholder={Utils.localizeMessage('login.password', 'Password')} + spellCheck='false' /> </div> - {ldapLogin} + <div className='form-group'> + <button + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='login.signIn' + defaultMessage='Sign in' + /> + </button> + </div> </div> - ); - } + </form> + ); } const userSignUp = ( @@ -358,14 +380,13 @@ export default class Login extends React.Component { <div> {extraBox} {oauthLogins} - {emailLogin} - {usernameLogin} - {ldapLogin} + {login} {userSignUp} {forgotPassword} </div> ); } + render() { let content; let customContent; @@ -373,7 +394,6 @@ export default class Login extends React.Component { if (this.state.showMfa) { content = ( <LoginMfa - method={this.state.method} loginId={this.state.loginId} password={this.state.password} submit={this.submit} diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx index 666e72e13..6dd26f391 100644 --- a/webapp/components/signup_user_complete.jsx +++ b/webapp/components/signup_user_complete.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import LoadingScreen from 'components/loading_screen.jsx'; -import LoginLdap from 'components/login/components/login_ldap.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import BrowserStore from 'stores/browser_store.jsx'; @@ -25,7 +24,6 @@ class SignupUserComplete extends React.Component { super(props); this.handleSubmit = this.handleSubmit.bind(this); - this.handleLdapSignup = this.handleLdapSignup.bind(this); this.state = { data: '', @@ -150,26 +148,6 @@ class SignupUserComplete extends React.Component { }); } - handleLdapSignup(method, loginId, password, token) { - Client.loginByLdap(loginId, password, token, - () => { - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - browserHistory.push(decodeURIComponent(redirect)); - } else { - GlobalActions.emitInitialLoad( - () => { - browserHistory.push('/select_team'); - } - ); - } - }, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - handleSubmit(e) { e.preventDefault(); @@ -268,15 +246,13 @@ class SignupUserComplete extends React.Component { this.state.data, this.state.hash, this.state.inviteId, - () => { + (data) => { Client.track('signup', 'signup_user_02_complete'); - Client.login( - user.email, - null, + Client.loginById( + data.id, user.password, '', () => { - UserStore.setLastEmail(user.email); if (this.state.hash > 0) { BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true})); } @@ -455,21 +431,6 @@ class SignupUserComplete extends React.Component { ); } - let ldapSignup; - if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { - ldapSignup = ( - <div className='inner__content'> - <h5><strong> - <FormattedMessage - id='signup_user_completed.withLdap' - defaultMessage='With your LDAP credentials' - /> - </strong></h5> - <LoginLdap submit={this.handleLdapSignup}/> - </div> - ); - } - let emailSignup; if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( @@ -533,7 +494,7 @@ class SignupUserComplete extends React.Component { ); } - if (signupMessage.length > 0 && (emailSignup || ldapSignup)) { + if (signupMessage.length > 0 && emailSignup) { signupMessage = ( <div> {signupMessage} @@ -547,21 +508,7 @@ class SignupUserComplete extends React.Component { ); } - if (ldapSignup && emailSignup) { - ldapSignup = ( - <div> - {ldapSignup} - <div className='or__container'> - <FormattedMessage - id='signup_user_completed.or' - defaultMessage='or' - /> - </div> - </div> - ); - } - - if (signupMessage.length === 0 && !emailSignup && !ldapSignup) { + if (signupMessage.length === 0 && !emailSignup) { emailSignup = ( <div> <FormattedMessage @@ -586,7 +533,6 @@ class SignupUserComplete extends React.Component { if (this.state.noOpenServerError) { signupMessage = null; - ldapSignup = null; emailSignup = null; terms = null; } @@ -620,7 +566,6 @@ class SignupUserComplete extends React.Component { /> </h4> {signupMessage} - {ldapSignup} {emailSignup} {serverError} {terms} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 3bceb978c..fbbf66fbd 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -953,41 +953,29 @@ "login.changed": " Sign-in method changed successfully", "login.create": "Create one now", "login.createTeam": "Create a new team", + "login.email": "Email", "login.find": "Find your other teams", "login.forgot": "I forgot my password", + "login.loginIdRequired": "A {type} is required", "login.gitlab": "with GitLab", "login.google": "with Google Apps", + "login.ldapUsername": "LDAP Username", "login.noAccount": "Don't have an account? ", "login.on": "on {siteName}", "login.or": "or", + "login.password": "Password", "login.passwordChanged": " Password updated successfully", + "login.passwordRequired": "A password is required", "login.session_expired": " Your session has expired. Please login again.", + "login.signIn": "Sign in", "login.signTo": "Sign in to:", + "login.username": "Username", + "login.userNotFound": "We couldn't find an existing account matching your login credentials.", "login.verified": " Email Verified", - "login_email.badTeam": "Bad team name", - "login_email.email": "Email", - "login_email.emailReq": "An email is required", - "login_email.pwd": "Password", - "login_email.pwdReq": "A password is required", - "login_email.signin": "Sign in", - "login_ldap.badTeam": "Bad team name", - "login_ldap.idlReq": "An LDAP ID is required", - "login_ldap.pwd": "LDAP Password", - "login_ldap.pwdReq": "An LDAP password is required", - "login_ldap.signin": "Sign in", - "login_ldap.username": "LDAP Username", "login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator", "login_mfa.submit": "Submit", "login_mfa.token": "MFA Token", "login_mfa.tokenReq": "Please enter an MFA token", - "login_username.badTeam": "Bad team name", - "login_username.pwd": "Password", - "login_username.pwdReq": "A password is required", - "login_username.signin": "Sign in", - "login_username.userNotFoundError": "We couldn't find an existing account matching your username for this team.", - "login_username.username": "Username", - "login_username.usernameReq": "A username is required", - "login_username.verifyEmailError": "Please verify your email address. Check your inbox for an email.", "member_item.makeAdmin": "Make Admin", "member_item.member": "Member", "member_list.noUsersAdd": "No users to add.", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 78645b769..34e4360b5 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -953,41 +953,27 @@ "login.changed": " Cambiado el método de inicio de sesión satisfactoriamente", "login.create": "Crea una ahora", "login.createTeam": "Crear un nuevo equipo", + "login.email": "Correo electrónico", "login.find": "Encuentra tus otros equipos", "login.forgot": "Olvide mi contraseña", "login.gitlab": "con GitLab", "login.google": "con Google Apps", + "login.ldapUsername": "Usuario LDAP", "login.noAccount": "¿No tienes una cuenta? ", "login.on": "en {siteName}", "login.or": "o", + "login.password": "Contraseña", "login.passwordChanged": " La contraseña ha sido actualizada", + "login.passwordRequired": "La contraseña es obligatoria", "login.session_expired": " Tu sesión ha expirado. Por favor inicia sesión nuevamente.", + "login.signIn": "Entrar", "login.signTo": "Ingresar a:", + "login.username": "Nombre de usuario", "login.verified": " Correo electrónico Verificado", - "login_email.badTeam": "Nombre de Equipo inválido", - "login_email.email": "Correo electrónico", - "login_email.emailReq": "El correo electrónico es obligatorio", - "login_email.pwd": "Contraseña", - "login_email.pwdReq": "La contraseña es obligatoria", - "login_email.signin": "Entrar", - "login_ldap.badTeam": "Nombre de Equipo inválido", - "login_ldap.idlReq": "El ID de LDAP es obligatorio", - "login_ldap.pwd": "Contraseña LDAP", - "login_ldap.pwdReq": "La contraseña LDAP es obligatoria", - "login_ldap.signin": "Entrar", - "login_ldap.username": "Usuario LDAP", "login_mfa.enterToken": "Para completar el proceso de inicio de sesión, por favor ingresa el token provisto por el autenticador de tu teléfono inteligente", "login_mfa.submit": "Enviar", "login_mfa.token": "Token AMF", "login_mfa.tokenReq": "Por favor ingresa un token AMF", - "login_username.badTeam": "Mal nombre de equipo", - "login_username.pwd": "Contraseña", - "login_username.pwdReq": "La contraseña es obligatoria", - "login_username.signin": "Ingresar", - "login_username.userNotFoundError": "No encontramos una cuenta existente que coincida con tu nombre de usuario en este equipo.", - "login_username.username": "Nombre de usuario", - "login_username.usernameReq": "El nombre de usuario es obligatorio", - "login_username.verifyEmailError": "Por favor válida tu dirección de correo electrónico. Te hemos enviado un correo, revisa tu bandeja de entrada.", "member_item.makeAdmin": "Convertir en Admin de Equipo", "member_item.member": "Miembro", "member_list.noUsersAdd": "No hay usuarios que agregar.", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index 39267f8b9..c92dff181 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -818,36 +818,22 @@ "login.changed": "Méthode de connexion changée", "login.create": "Créer maintenant", "login.createTeam": "Créer une nouvelle équipe", + "login.email": "Adresse électronique", "login.find": "Trouver vos autres équipes", "login.forgot": "Mot de passe perdu", "login.gitlab": "avec GitLab", "login.google": "avec Google Apps", + "login.ldapUsername": "Utilisateur LDAP", "login.noAccount": "Pas de compte utilisateur ?", "login.on": "sur {siteName}", "login.or": "ou", + "login.password": "Mot de passe", + "login.passwordRequired": "Le mot de passe est obligatoire", "login.session_expired": "Votre session a expiré. Merci de vous reconnecter.", + "login.signIn": "Connexion", "login.signTo": "Se connecter à :", + "login.username": "Nom d'utilisateur", "login.verified": "Adresse électronique vérifiée", - "login_email.badTeam": "Erreur dans le nom de l'équipe", - "login_email.email": "Adresse électronique", - "login_email.emailReq": "Une adresse électronique est obligatoire", - "login_email.pwd": "Mot de passe", - "login_email.pwdReq": "Le mot de passe est obligatoire", - "login_email.signin": "Connexion", - "login_ldap.badTeam": "Erreur dans le nom de l'équipe", - "login_ldap.idlReq": "Un identifiant LDAP est obligatoire", - "login_ldap.pwd": "Mot de passe LDAP", - "login_ldap.pwdReq": "Un mot de passe LDAP est obligatoire", - "login_ldap.signin": "Connexion", - "login_ldap.username": "Utilisateur LDAP", - "login_username.badTeam": "Erreur dans le nom de l'équipe", - "login_username.pwd": "Mot de passe", - "login_username.pwdReq": "Mot de passe obligatoire", - "login_username.signin": "Connexion", - "login_username.userNotFoundError": "Impossible de trouver un compte utilisateur à ce nom dans cette équipe.", - "login_username.username": "Nom d'utilisateur", - "login_username.usernameReq": "Le nom d'utilisateur est obligatoire", - "login_username.verifyEmailError": "Vérifiez votre adresse électronique. Cherchez le courriel dans votre boîte de réception.", "member_item.makeAdmin": "Passer Administrateur", "member_item.member": "Membre", "member_list.noUsersAdd": "Aucun utilisateur à ajouter.", diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index 290c6b5d5..88e10498e 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -915,40 +915,26 @@ "login.changed": "サインイン方法が変更されました", "login.create": "ただいま作成しています", "login.createTeam": "新しいチームを作成する", + "login.email": "電子メールアドレス", "login.find": "参加している他のチームを探す", "login.forgot": "パスワードを忘れました", "login.gitlab": "GitLabでログインする", "login.google": "Google Appsでログインする", + "login.ldapUsername": "LDAPユーザー名", "login.noAccount": "アカウントを持っていますか? ", "login.on": "{siteName}にて", "login.or": "または", + "login.password": "パスワード", + "login.passwordRequired": "パスワードは必須です", "login.session_expired": " あなたのセッションは有効期限が切れました。再度ログインしてください。", + "login.signIn": "利用登録", "login.signTo": "利用登録先: ", + "login.username": "LDAPユーザー名", "login.verified": " 電子メールアドレスが確認されました", - "login_email.badTeam": "無効なチーム名です", - "login_email.email": "電子メールアドレス", - "login_email.emailReq": "電子メールアドレスは必須です", - "login_email.pwd": "パスワード", - "login_email.pwdReq": "パスワードは必須です", - "login_email.signin": "利用登録", - "login_ldap.badTeam": "無効なチーム名です", - "login_ldap.idlReq": "LDAP IDは必須です", - "login_ldap.pwd": "LDAPパスワード", - "login_ldap.pwdReq": "LDAPパスワードは必須です", - "login_ldap.signin": "利用登録", - "login_ldap.username": "LDAPユーザー名", "login_mfa.enterToken": "利用登録を完了するには、スマートフォンのauthenticatorからのトークンを入力してください。", "login_mfa.submit": "送信する", "login_mfa.token": "MFAトークン", "login_mfa.tokenReq": "MFAトークンを入力してください", - "login_username.badTeam": "無効なチーム名です", - "login_username.pwd": "パスワード", - "login_username.pwdReq": "パスワードは必須です", - "login_username.signin": "利用登録", - "login_username.userNotFoundError": "このチームにはあなたのユーザー名に合致するアカウントはありません。", - "login_username.username": "ユーザー名", - "login_username.usernameReq": "ユーザー名は必須です", - "login_username.verifyEmailError": "電子メールアドレスを確認します。電子メールの受信ボックスを見てみてください。", "member_item.makeAdmin": "管理者にする", "member_item.member": "メンバー", "member_list.noUsersAdd": "ユーザーは追加されません。", diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json index 8daf1fe70..2edbc40e9 100644 --- a/webapp/i18n/pt.json +++ b/webapp/i18n/pt.json @@ -911,40 +911,26 @@ "login.changed": " Método de login alterada com sucesso", "login.create": "Crie um agora", "login.createTeam": "Criar uma nova equipe", + "login.email": "E-mail", "login.find": "Encontre suas outras equipes", "login.forgot": "Eu esqueci a minha senha", "login.gitlab": "com GitLab", "login.google": "com Google Apps", + "login.ldapUsername": "Usuário LDAP", "login.noAccount": "Não tem uma conta? ", "login.on": "no {siteName}", "login.or": "ou", + "login.password": "Senha", + "login.passwordRequired": "É necessário uma senha", "login.session_expired": " Sua sessão expirou. Por favor faça login novamente.", + "login.signIn": "Login", "login.signTo": "Login em:", + "login.username": "Usuário", "login.verified": " Email Verificado", - "login_email.badTeam": "Nome ruim de equipe", - "login_email.email": "E-mail", - "login_email.emailReq": "Um email é necessário", - "login_email.pwd": "Senha", - "login_email.pwdReq": "É necessário uma senha", - "login_email.signin": "Login", - "login_ldap.badTeam": "Nome de equipe ruim", - "login_ldap.idlReq": "Um ID LDAP é necessário", - "login_ldap.pwd": "Senha LDAP", - "login_ldap.pwdReq": "É necessário uma senha do LDAP", - "login_ldap.signin": "Login", - "login_ldap.username": "Usuário LDAP", "login_mfa.enterToken": "Para completar o login em processo, por favor entre um token do seu autenticador no smartphone", "login_mfa.submit": "Enviar", "login_mfa.token": "Token MFA", "login_mfa.tokenReq": "Por favor entre um token MFA", - "login_username.badTeam": "Nome de equipe ruim", - "login_username.pwd": "Senha", - "login_username.pwdReq": "É necessário uma senha", - "login_username.signin": "Login", - "login_username.userNotFoundError": "Não foi possível encontrar uma conta existente que corresponda ao seu usuário para esta equipe.", - "login_username.username": "Usuário", - "login_username.usernameReq": "Um nome de usuário é necessário", - "login_username.verifyEmailError": "Por favor verifique seu endereço de email. Verifique por um email na sua caixa de entrada.", "member_item.makeAdmin": "Tornar Admin", "member_item.member": "Membro", "member_list.noUsersAdd": "Nenhum usuário para adicionar.", diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 2c6bfade3..8d685aea3 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; @@ -97,22 +96,6 @@ class UserStoreClass extends EventEmitter { return null; } - getLastEmail() { - return BrowserStore.getGlobalItem('last_email', ''); - } - - setLastEmail(email) { - BrowserStore.setGlobalItem('last_email', email); - } - - getLastUsername() { - return BrowserStore.getGlobalItem('last_username', ''); - } - - setLastUsername(username) { - BrowserStore.setGlobalItem('last_username', username); - } - hasProfile(userId) { return this.getProfiles()[userId] != null; } diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx index e0ead2de9..7835a38bf 100644 --- a/webapp/tests/client_user.test.jsx +++ b/webapp/tests/client_user.test.jsx @@ -66,7 +66,6 @@ describe('Client.User', function() { function() { client.login( user.email, - null, user.password, null, function(data) { @@ -85,15 +84,15 @@ describe('Client.User', function() { ); }); - it('loginByUsername', function(done) { + it('loginById', function(done) { var client = TestHelper.createClient(); var user = TestHelper.fakeUser(); client.createUser( user, - function() { - client.login( - null, - user.username, + function(newUser) { + assert.equal(user.email, newUser.email); + client.loginById( + newUser.id, user.password, null, function(data) { @@ -112,14 +111,14 @@ describe('Client.User', function() { ); }); - it('loginByLdap', function(done) { + it('loginByUsername', function(done) { var client = TestHelper.createClient(); client.enableLogErrorsToConsole(false); // Disabling since this unit test causes an error var user = TestHelper.fakeUser(); client.createUser( user, function() { - client.loginByLdap( + client.login( user.username, user.password, null, @@ -127,7 +126,8 @@ describe('Client.User', function() { done(new Error()); }, function(err) { - assert.equal(err.id, 'api.user.login_ldap.disabled.app_error'); + // should error out because logging in by username is disabled by default + assert.equal(err.id, 'store.sql_user.get_for_login.app_error'); done(); } ); @@ -372,7 +372,6 @@ describe('Client.User', function() { it('checkMfa', function(done) { TestHelper.initBasic(() => { TestHelper.basicClient().checkMfa( - 'email', TestHelper.generateId(), function(data) { assert.equal(data.mfa_required, 'false'); diff --git a/webapp/tests/test_helper.jsx b/webapp/tests/test_helper.jsx index 385279360..f19d96433 100644 --- a/webapp/tests/test_helper.jsx +++ b/webapp/tests/test_helper.jsx @@ -128,7 +128,6 @@ class TestHelperClass { outer.basicClient().setTeamId(outer.basict.id); outer.basicClient().login( rteamSignup.user.email, - null, password, null, function() { diff --git a/webapp/utils/web_client.jsx b/webapp/utils/web_client.jsx index a9851442b..3efb32806 100644 --- a/webapp/utils/web_client.jsx +++ b/webapp/utils/web_client.jsx @@ -39,14 +39,13 @@ class WebClientClass extends Client { // not sure why but super.login doesn't work if using an () => arrow functions. // I think this might be a webpack issue. - webLogin(email, username, password, token, success, error) { + webLogin(loginId, password, token, success, error) { this.login( - email, - username, + loginId, password, token, (data) => { - this.track('api', 'api_users_login_success', '', 'email', data.email); + this.track('api', 'api_users_login_success', '', 'login_id', loginId); BrowserStore.signalLogin(); if (success) { @@ -54,7 +53,7 @@ class WebClientClass extends Client { } }, (err) => { - this.track('api', 'api_users_login_fail', name, 'email', email); + this.track('api', 'api_users_login_fail', name, 'login_id', loginId); if (error) { error(err); } |