// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api import ( "bytes" b64 "encoding/base64" "fmt" l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" "github.com/golang/freetype" "github.com/gorilla/mux" "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "hash/fnv" "image" "image/color" "image/draw" _ "image/gif" _ "image/jpeg" "image/png" "io" "io/ioutil" "net/http" "net/url" "strconv" "strings" ) func InitUser(r *mux.Router) { l4g.Debug("Initializing user api routes") sr := r.PathPrefix("/users").Subrouter() sr.Handle("/create", ApiAppHandler(createUser)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateUser)).Methods("POST") sr.Handle("/update_roles", ApiUserRequired(updateRoles)).Methods("POST") sr.Handle("/update_active", ApiUserRequired(updateActive)).Methods("POST") sr.Handle("/update_notify", ApiUserRequired(updateUserNotify)).Methods("POST") sr.Handle("/newpassword", ApiUserRequired(updatePassword)).Methods("POST") sr.Handle("/send_password_reset", ApiAppHandler(sendPasswordReset)).Methods("POST") sr.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST") sr.Handle("/login", ApiAppHandler(login)).Methods("POST") sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST") sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST") sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET") sr.Handle("/{id:[A-Za-z0-9]+}/image", ApiUserRequired(getProfileImage)).Methods("GET") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation { c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented return } user := model.UserFromJson(r.Body) if user == nil { c.SetInvalidParam("createUser", "user") return } // the user's username is checked to be valid when they are saved to the database user.EmailVerified = false var team *model.Team if result := <-Srv.Store.Team().Get(user.TeamId); result.Err != nil { c.Err = result.Err return } else { team = result.Data.(*model.Team) } hash := r.URL.Query().Get("h") sendWelcomeEmail := true if IsVerifyHashRequired(user, team, hash) { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "") return } t, err := strconv.ParseInt(props["time"], 10, 64) if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours c.Err = model.NewAppError("createUser", "The signup link has expired", "") return } if user.TeamId != props["id"] { c.Err = model.NewAppError("createUser", "Invalid team name", data) return } user.Email = props["email"] user.EmailVerified = true sendWelcomeEmail = false } if user.IsSSOUser() { user.EmailVerified = true } if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) { c.Err = model.NewAppError("createUser", "The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email.", "") return } ruser, err := CreateUser(team, user) if err != nil { c.Err = err return } if sendWelcomeEmail { sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified) } w.Write([]byte(ruser.ToJson())) } func CheckUserDomain(user *model.User, domains string) bool { if len(domains) == 0 { return true } domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1)))) matched := false for _, d := range domainArray { if strings.HasSuffix(user.Email, "@"+d) { matched = true break } } return matched } func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool { shouldVerifyHash := true if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil { matched := CheckUserDomain(user, team.AllowedDomains) if matched { shouldVerifyHash = false } else { return true } } if team.Type == model.TEAM_OPEN { shouldVerifyHash = false } if len(hash) > 0 { shouldVerifyHash = true } return shouldVerifyHash } func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppError) { channelRole := "" if team.Email == user.Email { user.Roles = model.ROLE_TEAM_ADMIN channelRole = model.CHANNEL_ROLE_ADMIN // Below is a speical case where the first user in the entire // system is granted the system_admin role instead of admin if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil { return nil, result.Err } else { count := result.Data.(int64) if count <= 0 { user.Roles = model.ROLE_SYSTEM_ADMIN } } } else { user.Roles = "" } user.MakeNonNil() if result := <-Srv.Store.User().Save(user); result.Err != nil { l4g.Error("Couldn't save the user err=%v", result.Err) return nil, result.Err } else { ruser := result.Data.(*model.User) // Soft error if there is an issue joining the default channels if err := JoinDefaultChannels(ruser, channelRole); err != nil { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } addDirectChannelsAndForget(ruser) if user.EmailVerified { if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { l4g.Error("Failed to set email verified err=%v", cresult.Err) } } pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"} if presult := <-Srv.Store.Preference().Save(&model.Preferences{pref}); presult.Err != nil { l4g.Error("Encountered error saving tutorial preference, err=%v", presult.Err.Message) } ruser.Sanitize(map[string]bool{}) // This message goes to every channel, so the channelId is irrelevant message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER) PublishAndForget(message) return ruser, nil } } func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { var user *model.User provider := einterfaces.GetOauthProvider(service) if provider == nil { c.Err = model.NewAppError("CreateOAuthUser", service+" oauth not avlailable on this server", "") return nil } else { user = provider.GetUserFromJson(userData) } if user == nil { c.Err = model.NewAppError("CreateOAuthUser", "Could not create user out of "+service+" user object", "") return nil } suchan := Srv.Store.User().GetByAuth(team.Id, user.AuthData, service) euchan := Srv.Store.User().GetByEmail(team.Id, user.Email) if team.Email == "" { team.Email = user.Email if result := <-Srv.Store.Team().Update(team); result.Err != nil { c.Err = result.Err return nil } } else { found := true count := 0 for found { if found = IsUsernameTaken(user.Username, team.Id); c.Err != nil { return nil } else if found { user.Username = user.Username + strconv.Itoa(count) count += 1 } } } if result := <-suchan; result.Err == nil { c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) return nil } if result := <-euchan; result.Err == nil { c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) return nil } user.TeamId = team.Id user.EmailVerified = true ruser, err := CreateUser(team, user) if err != nil { c.Err = err return nil } Login(c, w, r, ruser, "") if c.Err != nil { return nil } return ruser } func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { subjectPage := NewServerTemplatePage("welcome_subject") subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("welcome_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamURL"] = teamURL if !verified { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) bodyPage.Props["VerifyUrl"] = link } if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send welcome email successfully err=%v", err) } }() } func addDirectChannelsAndForget(user *model.User) { go func() { var profiles map[string]*model.User if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { l4g.Error("Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) return } else { profiles = result.Data.(map[string]*model.User) } var preferences model.Preferences for id := range profiles { if id == user.Id { continue } profile := profiles[id] preference := model.Preference{ UserId: user.Id, Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, Name: profile.Id, Value: "true", } preferences = append(preferences, preference) if len(preferences) >= 10 { break } } if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { l4g.Error("Failed to add direct channel preferences for new user user_id=%s, eam_id=%s, err=%v", user.Id, user.TeamId, result.Err.Error()) } }() } func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) subjectPage := NewServerTemplatePage("verify_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("verify_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["VerifyUrl"] = link if err := utils.SendMail(userEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send verification email successfully err=%v", err) } }() } func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User { if result := <-Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return nil } else { user := result.Data.(*model.User) if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { Login(c, w, r, user, deviceId) return user } } return nil } func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User { var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err return nil } else { team = result.Data.(*model.Team) } if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusForbidden return nil } else { user := result.Data.(*model.User) if len(user.AuthData) != 0 { c.Err = model.NewAppError("LoginByEmail", "Please sign in using "+user.AuthService, "") return nil } if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) { Login(c, w, r, user, deviceId) return user } } return nil } func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User { authData := "" provider := einterfaces.GetOauthProvider(service) if provider == nil { c.Err = model.NewAppError("LoginByOAuth", service+" oauth not avlailable on this server", "") return nil } else { authData = provider.GetAuthDataFromJson(userData) } if len(authData) == 0 { c.Err = model.NewAppError("LoginByOAuth", "Could not parse auth data out of "+service+" user object", "") return nil } var user *model.User if result := <-Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { c.Err = result.Err return nil } else { user = result.Data.(*model.User) Login(c, w, r, user, "") return user } } func checkUserLoginAttempts(c *Context, user *model.User) bool { if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts { c.LogAuditWithUserId(user.Id, "fail") c.Err = model.NewAppError("checkUserLoginAttempts", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden return false } return true } func checkUserPassword(c *Context, user *model.User, password string) bool { if !model.ComparePassword(user.Password, password) { c.LogAuditWithUserId(user.Id, "fail") c.Err = model.NewAppError("checkUserPassword", "Login failed because of invalid password", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil { c.LogError(result.Err) } return false } else { if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil { c.LogError(result.Err) } return true } } // User MUST be validated before calling Login func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) { c.LogAuditWithUserId(user.Id, "attempt") if !user.EmailVerified && utils.Cfg.EmailSettings.RequireEmailVerification { c.Err = model.NewAppError("Login", "Login failed because email address has not been verified", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden return } if user.DeleteAt > 0 { c.Err = model.NewAppError("Login", "Login failed because your account has been set to inactive. Please contact an administrator.", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden return } session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24 if len(deviceId) > 0 { session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays) maxAge = *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24 // A special case where we logout of all other sessions with the same Id if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusForbidden return } else { sessions := result.Data.([]*model.Session) for _, session := range sessions { if session.DeviceId == deviceId { l4g.Debug("Revoking sessionId=" + session.Id + " for userId=" + user.Id + " re-login with same device Id") RevokeSessionById(c, session.Id) if c.Err != nil { c.LogError(c.Err) c.Err = nil } } } } } 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)) if result := <-Srv.Store.Session().Save(session); result.Err != nil { c.Err = result.Err c.Err.StatusCode = http.StatusForbidden return } else { session = result.Data.(*model.Session) AddSessionToCache(session) } w.Header().Set(model.HEADER_TOKEN, session.Token) tokens := GetMultiSessionCookieTokens(r) multiToken := "" seen := make(map[string]string) seen[session.TeamId] = session.TeamId for _, token := range tokens { s := GetSession(token) if s != nil && !s.IsExpired() && seen[s.TeamId] == "" { multiToken += " " + token seen[s.TeamId] = s.TeamId } } multiToken = strings.TrimSpace(multiToken + " " + session.Token) multiSessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, Value: multiToken, Path: "/", MaxAge: maxAge, HttpOnly: true, } http.SetCookie(w, multiSessionCookie) c.Session = *session c.LogAuditWithUserId(user.Id, "success") } func login(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) if len(props["password"]) == 0 { c.Err = model.NewAppError("login", "Password field must not be blank", "") c.Err.StatusCode = http.StatusForbidden return } var user *model.User if len(props["id"]) != 0 { user = LoginById(c, w, r, props["id"], props["password"], props["device_id"]) } else if len(props["email"]) != 0 && len(props["name"]) != 0 { user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"]) } else { c.Err = model.NewAppError("login", "Either user id or team name and user email must be provided", "") c.Err.StatusCode = http.StatusForbidden return } if c.Err != nil { return } if user != nil { user.Sanitize(map[string]bool{}) } else { user = &model.User{} } w.Write([]byte(user.ToJson())) } func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) { if !*utils.Cfg.LdapSettings.Enable { c.Err = model.NewAppError("loginLdap", "LDAP not enabled on this server", "") c.Err.StatusCode = http.StatusNotImplemented return } props := model.MapFromJson(r.Body) password := props["password"] id := props["id"] teamName := props["teamName"] if len(password) == 0 { c.Err = model.NewAppError("loginLdap", "Password field must not be blank", "") c.Err.StatusCode = http.StatusForbidden return } if len(id) == 0 { c.Err = model.NewAppError("loginLdap", "Need an ID", "") c.Err.StatusCode = http.StatusForbidden return } teamc := Srv.Store.Team().GetByName(teamName) ldapInterface := einterfaces.GetLdapInterface() if ldapInterface == nil { c.Err = model.NewAppError("loginLdap", "LDAP not available on this server", "") c.Err.StatusCode = http.StatusNotImplemented return } var team *model.Team if result := <-teamc; result.Err != nil { c.Err = result.Err return } else { team = result.Data.(*model.Team) } user, err := ldapInterface.DoLogin(team, id, password) if err != nil { c.Err = err return } if !checkUserLoginAttempts(c, user) { return } // User is authenticated at this point Login(c, w, r, user, props["device_id"]) if user != nil { user.Sanitize(map[string]bool{}) } else { user = &model.User{} } w.Write([]byte(user.ToJson())) } func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) id := props["id"] RevokeSessionById(c, id) w.Write([]byte(model.MapToJson(props))) } func RevokeSessionById(c *Context, sessionId string) { if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil { c.Err = result.Err } else { session := result.Data.(*model.Session) c.LogAudit("session_id=" + session.Id) if session.IsOAuth { RevokeAccessToken(session.Token) } else { sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err } } } } func RevokeAllSession(c *Context, userId string) { if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { c.Err = result.Err return } else { sessions := result.Data.([]*model.Session) for _, session := range sessions { c.LogAuditWithUserId(userId, "session_id="+session.Id) if session.IsOAuth { RevokeAccessToken(session.Token) } else { sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return } } } } } func getSessions(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] if !c.HasPermissionsToUser(id, "getSessions") { return } if result := <-Srv.Store.Session().GetSessions(id); result.Err != nil { c.Err = result.Err return } else { sessions := result.Data.([]*model.Session) for _, session := range sessions { session.Sanitize() } w.Write([]byte(model.SessionsToJson(sessions))) } } func logout(c *Context, w http.ResponseWriter, r *http.Request) { data := make(map[string]string) data["user_id"] = c.Session.UserId Logout(c, w, r) if c.Err == nil { w.Write([]byte(model.MapToJson(data))) } } func Logout(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("") c.RemoveSessionCookie(w, r) if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { c.Err = result.Err return } } func getMe(c *Context, w http.ResponseWriter, r *http.Request) { if len(c.Session.UserId) == 0 { return } if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { return } else { result.Data.(*model.User).Sanitize(map[string]bool{}) w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag()) w.Header().Set("Expires", "-1") w.Write([]byte(result.Data.(*model.User).ToJson())) return } } func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] if !c.HasPermissionsToUser(id, "getUser") { return } if result := <-Srv.Store.User().Get(id); result.Err != nil { c.Err = result.Err return } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { return } else { result.Data.(*model.User).Sanitize(map[string]bool{}) w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag()) w.Write([]byte(result.Data.(*model.User).ToJson())) return } } func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id, ok := params["id"] if ok { // You must be system admin to access another team if id != c.Session.TeamId { if !c.HasSystemAdminPermissions("getProfiles") { return } } } else { id = c.Session.TeamId } etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string) if HandleEtag(etag, w, r) { return } if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil { c.Err = result.Err return } else { profiles := result.Data.(map[string]*model.User) for k, p := range profiles { options := utils.Cfg.GetSanitizeOptions() options["passwordupdate"] = false if c.IsSystemAdmin() { options["fullname"] = true options["email"] = true } p.Sanitize(options) p.ClearNonProfileFields() profiles[k] = p } w.Header().Set(model.HEADER_ETAG_SERVER, etag) w.Write([]byte(model.UserMapToJson(profiles))) return } } func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] if !c.HasPermissionsToUser(id, "getAudits") { return } userChan := Srv.Store.User().Get(id) auditChan := Srv.Store.Audit().Get(id, 20) if c.Err = (<-userChan).Err; c.Err != nil { return } if result := <-auditChan; result.Err != nil { c.Err = result.Err return } else { audits := result.Data.(model.Audits) etag := audits.Etag() if HandleEtag(etag, w, r) { return } if len(etag) > 0 { w.Header().Set(model.HEADER_ETAG_SERVER, etag) } w.Write([]byte(audits.ToJson())) return } } func createProfileImage(username string, userId string) ([]byte, *model.AppError) { colors := []color.NRGBA{ {197, 8, 126, 255}, {227, 207, 18, 255}, {28, 181, 105, 255}, {35, 188, 224, 255}, {116, 49, 196, 255}, {197, 8, 126, 255}, {197, 19, 19, 255}, {250, 134, 6, 255}, {227, 207, 18, 255}, {123, 201, 71, 255}, {28, 181, 105, 255}, {35, 188, 224, 255}, {116, 49, 196, 255}, {197, 8, 126, 255}, {197, 19, 19, 255}, {250, 134, 6, 255}, {227, 207, 18, 255}, {123, 201, 71, 255}, {28, 181, 105, 255}, {35, 188, 224, 255}, {116, 49, 196, 255}, {197, 8, 126, 255}, {197, 19, 19, 255}, {250, 134, 6, 255}, {227, 207, 18, 255}, {123, 201, 71, 255}, } h := fnv.New32a() h.Write([]byte(userId)) seed := h.Sum32() initial := string(strings.ToUpper(username)[0]) fontBytes, err := ioutil.ReadFile(utils.FindDir("web/static/fonts") + utils.Cfg.FileSettings.InitialFont) if err != nil { return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) } font, err := freetype.ParseFont(fontBytes) if err != nil { return nil, model.NewAppError("createProfileImage", "Could not create default profile image font", err.Error()) } width := int(utils.Cfg.FileSettings.ProfileWidth) height := int(utils.Cfg.FileSettings.ProfileHeight) color := colors[int64(seed)%int64(len(colors))] dstImg := image.NewRGBA(image.Rect(0, 0, width, height)) srcImg := image.White draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src) size := float64((width + height) / 4) c := freetype.NewContext() c.SetFont(font) c.SetFontSize(size) c.SetClip(dstImg.Bounds()) c.SetDst(dstImg) c.SetSrc(srcImg) pt := freetype.Pt(width/6, height*2/3) _, err = c.DrawString(initial, pt) if err != nil { return nil, model.NewAppError("createProfileImage", "Could not add user initial to default profile picture", err.Error()) } buf := new(bytes.Buffer) if imgErr := png.Encode(buf, dstImg); imgErr != nil { return nil, model.NewAppError("createProfileImage", "Could not encode default profile image", imgErr.Error()) } else { return buf.Bytes(), nil } } func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] if result := <-Srv.Store.User().Get(id); result.Err != nil { c.Err = result.Err return } else { var img []byte if len(utils.Cfg.FileSettings.DriverName) == 0 { var err *model.AppError if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { c.Err = err return } } else { path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png" if data, err := readFile(path); err != nil { if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { c.Err = err return } if err := writeFile(img, path); err != nil { c.Err = err return } } else { img = data } } if c.Session.UserId == id { w.Header().Set("Cache-Control", "max-age=300, public") // 5 mins } else { w.Header().Set("Cache-Control", "max-age=86400, public") // 24 hrs } w.Header().Set("Content-Type", "image/png") w.Write(img) } } func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { if len(utils.Cfg.FileSettings.DriverName) == 0 { c.Err = model.NewAppError("uploadProfileImage", "Unable to upload file. Image storage is not configured.", "") c.Err.StatusCode = http.StatusNotImplemented return } if err := r.ParseMultipartForm(10000000); err != nil { c.Err = model.NewAppError("uploadProfileImage", "Could not parse multipart form", "") return } m := r.MultipartForm imageArray, ok := m.File["image"] if !ok { c.Err = model.NewAppError("uploadProfileImage", "No file under 'image' in request", "") c.Err.StatusCode = http.StatusBadRequest return } if len(imageArray) <= 0 { c.Err = model.NewAppError("uploadProfileImage", "Empty array under 'image' in request", "") c.Err.StatusCode = http.StatusBadRequest return } imageData := imageArray[0] file, err := imageData.Open() defer file.Close() if err != nil { c.Err = model.NewAppError("uploadProfileImage", "Could not open image file", err.Error()) return } // Decode image config first to check dimensions before loading the whole thing into memory later on config, _, err := image.DecodeConfig(file) if err != nil { c.Err = model.NewAppError("uploadProfileFile", "Could not decode profile image config.", err.Error()) return } else if config.Width*config.Height > MaxImageSize { c.Err = model.NewAppError("uploadProfileFile", "Unable to upload profile image. File is too large.", err.Error()) return } file.Seek(0, 0) // Decode image into Image object img, _, err := image.Decode(file) if err != nil { c.Err = model.NewAppError("uploadProfileImage", "Could not decode profile image", err.Error()) return } // Scale profile image img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos) buf := new(bytes.Buffer) err = png.Encode(buf, img) if err != nil { c.Err = model.NewAppError("uploadProfileImage", "Could not encode profile image", err.Error()) return } path := "teams/" + c.Session.TeamId + "/users/" + c.Session.UserId + "/profile.png" if err := writeFile(buf.Bytes(), path); err != nil { c.Err = model.NewAppError("uploadProfileImage", "Couldn't upload profile image", "") return } Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId) c.LogAudit("") // write something as the response since jQuery expects a json response w.Write([]byte("true")) } func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { user := model.UserFromJson(r.Body) if user == nil { c.SetInvalidParam("updateUser", "user") return } if !c.HasPermissionsToUser(user.Id, "updateUser") { return } if result := <-Srv.Store.User().Update(user, false); result.Err != nil { c.Err = result.Err return } else { c.LogAudit("") rusers := result.Data.([2]*model.User) if rusers[0].Email != rusers[1].Email { if tresult := <-Srv.Store.Team().Get(rusers[1].TeamId); tresult.Err != nil { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) if utils.Cfg.EmailSettings.RequireEmailVerification { SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } } } rusers[0].Password = "" rusers[0].AuthData = "" w.Write([]byte(rusers[0].ToJson())) } } func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempted") props := model.MapFromJson(r.Body) userId := props["user_id"] if len(userId) != 26 { c.SetInvalidParam("updatePassword", "user_id") return } currentPassword := props["current_password"] if len(currentPassword) <= 0 { c.SetInvalidParam("updatePassword", "current_password") return } newPassword := props["new_password"] if len(newPassword) < 5 { c.SetInvalidParam("updatePassword", "new_password") return } if userId != c.Session.UserId { c.Err = model.NewAppError("updatePassword", "Update password failed because context user_id did not match props user_id", "") c.Err.StatusCode = http.StatusForbidden return } var result store.StoreResult if result = <-Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return } if result.Data == nil { c.Err = model.NewAppError("updatePassword", "Update password failed because we couldn't find a valid account", "") c.Err.StatusCode = http.StatusBadRequest return } user := result.Data.(*model.User) tchan := Srv.Store.Team().Get(user.TeamId) if user.AuthData != "" { c.LogAudit("failed - tried to update user password who was logged in through oauth") c.Err = model.NewAppError("updatePassword", "Update password failed because the user is logged in through an OAuth service", "auth_service="+user.AuthService) c.Err.StatusCode = http.StatusForbidden return } if !model.ComparePassword(user.Password, currentPassword) { c.Err = model.NewAppError("updatePassword", "The \"Current Password\" you entered is incorrect. Please check that Caps Lock is off and try again.", "") c.Err.StatusCode = http.StatusForbidden return } if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil { c.Err = model.NewAppError("updatePassword", "Update password failed", uresult.Err.Error()) c.Err.StatusCode = http.StatusForbidden return } else { c.LogAudit("completed") if tresult := <-tchan; tresult.Err != nil { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu") } data := make(map[string]string) data["user_id"] = uresult.Data.(string) w.Write([]byte(model.MapToJson(data))) } } func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) user_id := props["user_id"] if len(user_id) != 26 { c.SetInvalidParam("updateRoles", "user_id") return } new_roles := props["new_roles"] if !model.IsValidRoles(new_roles) { c.SetInvalidParam("updateRoles", "new_roles") return } if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() { c.Err = model.NewAppError("updateRoles", "The system admin role can only be set by another system admin", "") c.Err.StatusCode = http.StatusForbidden return } var user *model.User if result := <-Srv.Store.User().Get(user_id); result.Err != nil { c.Err = result.Err return } else { user = result.Data.(*model.User) } if !c.HasPermissionsToTeam(user.TeamId, "updateRoles") { return } if !c.IsTeamAdmin() { c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id) c.Err.StatusCode = http.StatusForbidden return } if user.IsInRole(model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() { c.Err = model.NewAppError("updateRoles", "The system admin role can only by modified by another system admin", "") c.Err.StatusCode = http.StatusForbidden return } ruser := UpdateRoles(c, user, new_roles) if c.Err != nil { return } uchan := Srv.Store.Session().UpdateRoles(user.Id, new_roles) gchan := Srv.Store.Session().GetSessions(user.Id) if result := <-uchan; result.Err != nil { // soft error since the user roles were still updated l4g.Error(result.Err) } if result := <-gchan; result.Err != nil { // soft error since the user roles were still updated l4g.Error(result.Err) } else { sessions := result.Data.([]*model.Session) for _, s := range sessions { sessionCache.Remove(s.Token) } } options := utils.Cfg.GetSanitizeOptions() options["passwordupdate"] = false ruser.Sanitize(options) w.Write([]byte(ruser.ToJson())) } func UpdateRoles(c *Context, user *model.User, roles string) *model.User { // make sure there is at least 1 other active admin if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) { if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) { if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { c.Err = result.Err return nil } else { activeAdmins := -1 profileUsers := result.Data.(map[string]*model.User) for _, profileUser := range profileUsers { if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { activeAdmins = activeAdmins + 1 } } if activeAdmins <= 0 { c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "") return nil } } } } user.Roles = roles var ruser *model.User if result := <-Srv.Store.User().Update(user, true); result.Err != nil { c.Err = result.Err return nil } else { c.LogAuditWithUserId(user.Id, "roles="+roles) ruser = result.Data.([2]*model.User)[0] } return ruser } func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) user_id := props["user_id"] if len(user_id) != 26 { c.SetInvalidParam("updateActive", "user_id") return } active := props["active"] == "true" var user *model.User if result := <-Srv.Store.User().Get(user_id); result.Err != nil { c.Err = result.Err return } else { user = result.Data.(*model.User) } if !c.HasPermissionsToTeam(user.TeamId, "updateActive") { return } if !c.IsTeamAdmin() { c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id) c.Err.StatusCode = http.StatusForbidden return } // make sure there is at least 1 other active admin if !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) { if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { c.Err = result.Err return } else { activeAdmins := -1 profileUsers := result.Data.(map[string]*model.User) for _, profileUser := range profileUsers { if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { activeAdmins = activeAdmins + 1 } } if activeAdmins <= 0 { c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "userId="+user_id) return } } } ruser := UpdateActive(c, user, active) if c.Err == nil { w.Write([]byte(ruser.ToJson())) } } func UpdateActive(c *Context, user *model.User, active bool) *model.User { if active { user.DeleteAt = 0 } else { user.DeleteAt = model.GetMillis() } if result := <-Srv.Store.User().Update(user, true); result.Err != nil { c.Err = result.Err return nil } else { c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active)) if user.DeleteAt > 0 { RevokeAllSession(c, user.Id) } ruser := result.Data.([2]*model.User)[0] options := utils.Cfg.GetSanitizeOptions() options["passwordupdate"] = false ruser.Sanitize(options) return ruser } } func PermanentDeleteUser(c *Context, user *model.User) *model.AppError { l4g.Warn("Attempting to permanently delete account %v id=%v", user.Email, user.Id) c.Path = "/users/permanent_delete" c.LogAuditWithUserId(user.Id, fmt.Sprintf("attempt userId=%v", user.Id)) c.LogAuditWithUserId("", fmt.Sprintf("attempt userId=%v", user.Id)) if user.IsInRole(model.ROLE_SYSTEM_ADMIN) { l4g.Warn("You are deleting %v that is a system administrator. You may need to set another account as the system administrator using the command line tools.", user.Email) } UpdateActive(c, user, false) if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil { return result.Err } if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } l4g.Warn("Permanently deleted account %v id=%v", user.Email, user.Id) c.LogAuditWithUserId("", fmt.Sprintf("success userId=%v", user.Id)) return nil } func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) email := props["email"] if len(email) == 0 { c.SetInvalidParam("sendPasswordReset", "email") return } name := props["name"] if len(name) == 0 { c.SetInvalidParam("sendPasswordReset", "name") return } var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err return } else { team = result.Data.(*model.Team) } var user *model.User if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.Err = model.NewAppError("sendPasswordReset", "We couldn’t find an account with that address.", "email="+email+" team_id="+team.Id) return } else { user = result.Data.(*model.User) } if len(user.AuthData) != 0 { c.Err = model.NewAppError("sendPasswordReset", "Cannot reset password for SSO accounts", "userId="+user.Id+", teamId="+team.Id) return } newProps := make(map[string]string) newProps["user_id"] = user.Id newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) subjectPage := NewServerTemplatePage("reset_subject") subjectPage.Props["SiteURL"] = c.GetSiteURL() bodyPage := NewServerTemplatePage("reset_body") bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["ResetUrl"] = link if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { c.Err = model.NewAppError("sendPasswordReset", "Failed to send password reset email successfully", "err="+err.Message) return } c.LogAuditWithUserId(user.Id, "sent="+email) w.Write([]byte(model.MapToJson(props))) } func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) newPassword := props["new_password"] if len(newPassword) < 5 { c.SetInvalidParam("resetPassword", "new_password") return } name := props["name"] if len(name) == 0 { c.SetInvalidParam("resetPassword", "name") return } userId := props["user_id"] hash := props["hash"] timeStr := "" if !c.IsSystemAdmin() { if len(hash) == 0 { c.SetInvalidParam("resetPassword", "hash") return } data := model.MapFromJson(strings.NewReader(props["data"])) userId = data["user_id"] timeStr = data["time"] if len(timeStr) == 0 { c.SetInvalidParam("resetPassword", "data:time") return } } if len(userId) != 26 { c.SetInvalidParam("resetPassword", "user_id") return } c.LogAuditWithUserId(userId, "attempt") var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err return } else { team = result.Data.(*model.Team) } var user *model.User if result := <-Srv.Store.User().Get(userId); result.Err != nil { c.Err = result.Err return } else { user = result.Data.(*model.User) } if len(user.AuthData) != 0 { c.Err = model.NewAppError("resetPassword", "Cannot reset password for SSO accounts", "userId="+user.Id+", teamId="+team.Id) return } if user.TeamId != team.Id { c.Err = model.NewAppError("resetPassword", "Trying to reset password for user on wrong team.", "userId="+user.Id+", teamId="+team.Id) c.Err.StatusCode = http.StatusForbidden return } if !c.IsSystemAdmin() { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) { c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "") return } t, err := strconv.ParseInt(timeStr, 10, 64) if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour c.Err = model.NewAppError("resetPassword", "The reset link has expired", "") return } } if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { c.Err = result.Err return } else { c.LogAuditWithUserId(userId, "success") } sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") props["new_password"] = "" w.Write([]byte(model.MapToJson(props))) } func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { go func() { subjectPage := NewServerTemplatePage("password_change_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("password_change_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL bodyPage.Props["Method"] = method if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send update password email successfully err=%v", err) } }() } func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { subjectPage := NewServerTemplatePage("email_change_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("email_change_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL bodyPage.Props["NewEmail"] = newEmail if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send email change notification email successfully err=%v", err) } }() } func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) subjectPage := NewServerTemplatePage("email_change_verify_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("email_change_verify_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["VerifyUrl"] = link if err := utils.SendMail(newUserEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send email change verification email successfully err=%v", err) } }() } func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) user_id := props["user_id"] if len(user_id) != 26 { c.SetInvalidParam("updateUserNotify", "user_id") return } uchan := Srv.Store.User().Get(user_id) if !c.HasPermissionsToUser(user_id, "updateUserNotify") { return } delete(props, "user_id") email := props["email"] if len(email) == 0 { c.SetInvalidParam("updateUserNotify", "email") return } desktop_sound := props["desktop_sound"] if len(desktop_sound) == 0 { c.SetInvalidParam("updateUserNotify", "desktop_sound") return } desktop := props["desktop"] if len(desktop) == 0 { c.SetInvalidParam("updateUserNotify", "desktop") return } var user *model.User if result := <-uchan; result.Err != nil { c.Err = result.Err return } else { user = result.Data.(*model.User) } user.NotifyProps = props if result := <-Srv.Store.User().Update(user, false); result.Err != nil { c.Err = result.Err return } else { c.LogAuditWithUserId(user.Id, "") ruser := result.Data.([2]*model.User)[0] options := utils.Cfg.GetSanitizeOptions() options["passwordupdate"] = false ruser.Sanitize(options) w.Write([]byte(ruser.ToJson())) } } func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { userIds := model.ArrayFromJson(r.Body) if len(userIds) == 0 { c.SetInvalidParam("getStatuses", "userIds") return } if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil { c.Err = result.Err return } else { profiles := result.Data.(map[string]*model.User) statuses := map[string]string{} for _, profile := range profiles { found := false for _, uid := range userIds { if uid == profile.Id { found = true } } if !found { continue } if profile.IsOffline() { statuses[profile.Id] = model.USER_OFFLINE } else if profile.IsAway() { statuses[profile.Id] = model.USER_AWAY } else { statuses[profile.Id] = model.USER_ONLINE } } //w.Header().Set("Cache-Control", "max-age=9, public") // 2 mins w.Write([]byte(model.MapToJson(statuses))) return } } func GetAuthorizationCode(c *Context, service, teamName string, props map[string]string, loginHint string) (string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso != nil && !sso.Enable { return "", model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service) } clientId := sso.Id endpoint := sso.AuthEndpoint scope := sso.Scope props["hash"] = model.HashPassword(clientId) props["team"] = teamName state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) if len(scope) > 0 { authUrl += "&scope=" + utils.UrlEncode(scope) } if len(loginHint) > 0 { authUrl += "&login_hint=" + utils.UrlEncode(loginHint) } return authUrl, nil } func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, map[string]string, *model.AppError) { sso := utils.Cfg.GetSSOService(service) if sso == nil || !sso.Enable { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service) } stateStr := "" if b, err := b64.StdEncoding.DecodeString(state); err != nil { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error()) } else { stateStr = string(b) } stateProps := model.MapFromJson(strings.NewReader(stateStr)) if !model.ComparePassword(stateProps["hash"], sso.Id) { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "") } ok := true teamName := "" if teamName, ok = stateProps["team"]; !ok { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "") } tchan := Srv.Store.Team().GetByName(teamName) p := url.Values{} p.Set("client_id", sso.Id) p.Set("client_secret", sso.Secret) p.Set("code", code) p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) p.Set("redirect_uri", redirectUri) client := &http.Client{} req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") var ar *model.AccessResponse if resp, err := client.Do(req); err != nil { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) } else { ar = model.AccessResponseFromJson(resp.Body) if ar == nil { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "") } } if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) } if len(ar.AccessToken) == 0 { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") } p = url.Values{} p.Set("access_token", ar.AccessToken) req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+ar.AccessToken) if resp, err := client.Do(req); err != nil { return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) } else { if result := <-tchan; result.Err != nil { return nil, nil, nil, result.Err } else { return resp.Body, result.Data.(*model.Team), stateProps, nil } } } func IsUsernameTaken(name string, teamId string) bool { if !model.IsValidUsername(name) { return false } if result := <-Srv.Store.User().GetByUsername(teamId, name); result.Err != nil { return false } else { return true } return false } func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) password := props["password"] if len(password) == 0 { c.SetInvalidParam("switchToSSO", "password") return } teamName := props["team_name"] if len(teamName) == 0 { c.SetInvalidParam("switchToSSO", "team_name") return } service := props["service"] if len(service) == 0 { c.SetInvalidParam("switchToSSO", "service") return } email := props["email"] if len(email) == 0 { c.SetInvalidParam("switchToSSO", "email") return } c.LogAudit("attempt") var team *model.Team if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { c.LogAudit("fail - couldn't get team") c.Err = result.Err return } else { team = result.Data.(*model.Team) } var user *model.User if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return } else { user = result.Data.(*model.User) } if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, password) { c.LogAuditWithUserId(user.Id, "fail - invalid password") return } stateProps := map[string]string{} stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO stateProps["email"] = email m := map[string]string{} if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { c.LogAuditWithUserId(user.Id, "fail - oauth issue") c.Err = err return } else { m["follow_link"] = authUrl } c.LogAuditWithUserId(user.Id, "success") w.Write([]byte(model.MapToJson(m))) } func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) { authData := "" provider := einterfaces.GetOauthProvider(service) if provider == nil { c.Err = model.NewAppError("CompleteClaimWithOAuth", service+" oauth not avlailable on this server", "") return } else { authData = provider.GetAuthDataFromJson(userData) } if len(authData) == 0 { c.Err = model.NewAppError("CompleteClaimWithOAuth", "Could not parse auth data out of "+service+" user object", "") return } if len(email) == 0 { c.Err = model.NewAppError("CompleteClaimWithOAuth", "Blank email", "") return } var user *model.User if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.Err = result.Err return } else { user = result.Data.(*model.User) } RevokeAllSession(c, user.Id) if c.Err != nil { return } if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData); result.Err != nil { c.Err = result.Err return } sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") } func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) password := props["password"] if len(password) == 0 { c.SetInvalidParam("switchToEmail", "password") return } teamName := props["team_name"] if len(teamName) == 0 { c.SetInvalidParam("switchToEmail", "team_name") return } email := props["email"] if len(email) == 0 { c.SetInvalidParam("switchToEmail", "email") return } c.LogAudit("attempt") var team *model.Team if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { c.LogAudit("fail - couldn't get team") c.Err = result.Err return } else { team = result.Data.(*model.Team) } var user *model.User if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.LogAudit("fail - couldn't get user") c.Err = result.Err return } else { user = result.Data.(*model.User) } if user.Id != c.Session.UserId { c.LogAudit("fail - user ids didn't match") c.Err = model.NewAppError("switchToEmail", "Update password failed because context user_id did not match provided user's id", "") c.Err.StatusCode = http.StatusForbidden return } if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil { c.LogAudit("fail - database issue") c.Err = result.Err return } sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password") RevokeAllSession(c, c.Session.UserId) if c.Err != nil { return } m := map[string]string{} m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change" c.LogAudit("success") w.Write([]byte(model.MapToJson(m))) } func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { go func() { subjectPage := NewServerTemplatePage("signin_change_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("signin_change_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL bodyPage.Props["Method"] = method if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send update password email successfully err=%v", err) } }() }