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 --- app/authentication.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ app/file.go | 6 +- app/login.go | 136 ++++++++++++++++++++++++++++++++++++++ app/user.go | 108 ++++++++++++++++++++++++++++++ 4 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 app/authentication.go create mode 100644 app/login.go (limited to 'app') 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 -- cgit v1.2.3-1-g7c22