mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* improve performance on sendNotifications * Fix SQL queries * Remove get direct profiles, not needed anymore * Add raw data to error details if AppError fails to decode * men * Fix decode (#4052) * Fixing json decode * Adding unit test * Initial work for client scaling (#4051) * Begin adding paging to profiles API * Added more paging functionality * Finish hooking up admin console user lists * Add API for searching users and add searching to all user lists * Add lazy loading of profiles * Revert config.json * Fix unit tests and some style issues * Add GetProfilesFromList to Go driver and fix web unit test * Update etag for GetProfiles * Updating ui for filters and pagination (#4044) * Updating UI for pagination * Adjusting margins for filter row * Adjusting margin for specific modals * Adding relative padding to system console * Adjusting responsive view * Update client user tests * Minor fixes for direct messages modal (#4056) * Remove some unneeded initial load calls (#4057) * UX updates to user lists, added smart counts and bug fixes (#4059) * Improved getExplicitMentions and unit tests (#4064) * Refactor getting posts to lazy load profiles correctly (#4062) * Comment out SetActiveChannel test (#4066) * Profiler cpu, block, and memory profiler. (#4081) * Fix TestSetActiveChannel unit test (#4071) * Fixing build failure caused by dependancies updating (#4076) * Adding profiler * Fix admin_team_member_dropdown eslint errors * Bumping session cache size (#4077) * Bumping session cache size * Bumping status cache * Refactor how the client handles channel members to be large team friendly (#4106) * Refactor how the client handles channel members to be large team friendly * Change Id to ChannelId in ChannelStats model * Updated getChannelMember and getProfilesByIds routes to match proposal * Performance improvements (#4100) * Performance improvements * Fixing re-connect issue * Fixing error message * Some other minor perf tweaks * Some other minor perf tweaks * Fixing config file * Fixing buffer size * Fixing web socket send message * adding some error logging * fix getMe to be user required * Fix websocket event for new user * Fixing shutting down * Reverting web socket changes * Fixing logging lvl * Adding caching to GetMember * Adding some logging * Fixing caching * Fixing caching invalidate * Fixing direct message caching * Fixing caching * Fixing caching * Remove GetDirectProfiles from initial load * Adding logging and fixing websocket client * Adding back caching from bad merge. * Explicitly close go driver requests (#4162) * Refactored how the client handles team members to be more large team friendly (#4159) * Refactor getProfilesForDirectMessageList API into getAllProfiles API * Refactored how the client handles team members to be more large team friendly * Fix js error when receiving a notification * Fix JS error caused by current user being overwritten with sanitized version (#4165) * Adding error message to status failure (#4167) * Fix a few bugs caused by client scaling refactoring (#4170) * When there is no read replica, don't open a second set of connections to the master database (#4173) * Adding connection tacking to stats (#4174) * Reduce DB writes for statuses and other status related changes (#4175) * Fix bug preventing opening of DM channels from more modal (#4181) * Fixing socket timing error (#4183) * Fixing ping/pong handler * Fixing socket timing error * Commenting out status broadcasting * Removing user status changes * Removing user status changes * Removing user status changes * Removing user status changes * Adding DoPreComputeJson() * Performance improvements (#4194) * * Fix System Console Analytics queries * Add db.SetConnMaxLifetime to 15 minutes * Add "net/http/pprof" for profiling * Add FreeOSMemory() to manually release memory on reload config * Add flag to enable http profiler * Fix memory leak (#4197) * Fix memory leak * removed unneeded nil assignment * Fixing go routine leak (#4208) * Merge fixes * Merge fix * Refactored statuses to be queried by the client rather than broadcast by the server (#4212) * Refactored server code to reduce status broadcasts and to allow getting statuses by IDs * Refactor client code to periodically fetch statuses * Add store unit test for getting statuses by ids * Fix status unit test * Add getStatusesByIds REST API and move the client over to use that instead of the WebSocket * Adding multiple threads to websocket hub (#4230) * Adding multiple threads to websocket hub * Fixing unit tests * Fixing so websocket connections from the same user end up in the same… (#4240) * Fixing so websocket connections from the same user end up in the same list * Removing old comment * Refactor user autocomplete to query the server (#4239) * Add API for autocompleting users * Converted at mention autocomplete to query server * Converted user search autocomplete to query server * Switch autocomplete API naming to use term instead of username * Split autocomplete API into two, one for channels and for teams * Fix copy/paste error * Some final client scaling fixes (#4246) * Add lazy loading of profiles to integration pages * Add lazy loading of profiles to emoji page * Fix JS error when receiving post in select team menu and also clean up channel store
509 lines
12 KiB
Go
509 lines
12 KiB
Go
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
USER_NOTIFY_ALL = "all"
|
|
USER_NOTIFY_MENTION = "mention"
|
|
USER_NOTIFY_NONE = "none"
|
|
DEFAULT_LOCALE = "en"
|
|
USER_AUTH_SERVICE_EMAIL = "email"
|
|
USER_AUTH_SERVICE_USERNAME = "username"
|
|
)
|
|
|
|
type User struct {
|
|
Id string `json:"id"`
|
|
CreateAt int64 `json:"create_at,omitempty"`
|
|
UpdateAt int64 `json:"update_at,omitempty"`
|
|
DeleteAt int64 `json:"delete_at"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password,omitempty"`
|
|
AuthData *string `json:"auth_data,omitempty"`
|
|
AuthService string `json:"auth_service"`
|
|
Email string `json:"email"`
|
|
EmailVerified bool `json:"email_verified,omitempty"`
|
|
Nickname string `json:"nickname"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Roles string `json:"roles"`
|
|
AllowMarketing bool `json:"allow_marketing,omitempty"`
|
|
Props StringMap `json:"props,omitempty"`
|
|
NotifyProps StringMap `json:"notify_props,omitempty"`
|
|
LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
|
|
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
|
|
FailedAttempts int `json:"failed_attempts,omitempty"`
|
|
Locale string `json:"locale"`
|
|
MfaActive bool `json:"mfa_active,omitempty"`
|
|
MfaSecret string `json:"mfa_secret,omitempty"`
|
|
LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"`
|
|
}
|
|
|
|
// IsValid validates the user and returns an error if it isn't configured
|
|
// correctly.
|
|
func (u *User) IsValid() *AppError {
|
|
|
|
if len(u.Id) != 26 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "")
|
|
}
|
|
|
|
if u.CreateAt == 0 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if u.UpdateAt == 0 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if !IsValidUsername(u.Username) {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if len(u.Email) > 128 || len(u.Email) == 0 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if utf8.RuneCountInString(u.Nickname) > 64 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if utf8.RuneCountInString(u.FirstName) > 64 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if utf8.RuneCountInString(u.LastName) > 64 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if u.AuthData != nil && len(*u.AuthData) > 128 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 {
|
|
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PreSave will set the Id and Username if missing. It will also fill
|
|
// in the CreateAt, UpdateAt times. It will also hash the password. It should
|
|
// be run before saving the user to the db.
|
|
func (u *User) PreSave() {
|
|
if u.Id == "" {
|
|
u.Id = NewId()
|
|
}
|
|
|
|
if u.Username == "" {
|
|
u.Username = NewId()
|
|
}
|
|
|
|
if u.AuthData != nil && *u.AuthData == "" {
|
|
u.AuthData = nil
|
|
}
|
|
|
|
u.Username = strings.ToLower(u.Username)
|
|
u.Email = strings.ToLower(u.Email)
|
|
|
|
u.CreateAt = GetMillis()
|
|
u.UpdateAt = u.CreateAt
|
|
|
|
u.LastPasswordUpdate = u.CreateAt
|
|
|
|
u.MfaActive = false
|
|
|
|
if u.Locale == "" {
|
|
u.Locale = DEFAULT_LOCALE
|
|
}
|
|
|
|
if u.Props == nil {
|
|
u.Props = make(map[string]string)
|
|
}
|
|
|
|
if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
|
|
u.SetDefaultNotifications()
|
|
}
|
|
|
|
if len(u.Password) > 0 {
|
|
u.Password = HashPassword(u.Password)
|
|
}
|
|
}
|
|
|
|
// PreUpdate should be run before updating the user in the db.
|
|
func (u *User) PreUpdate() {
|
|
u.Username = strings.ToLower(u.Username)
|
|
u.Email = strings.ToLower(u.Email)
|
|
u.UpdateAt = GetMillis()
|
|
|
|
if u.AuthData != nil && *u.AuthData == "" {
|
|
u.AuthData = nil
|
|
}
|
|
|
|
if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
|
|
u.SetDefaultNotifications()
|
|
} else if _, ok := u.NotifyProps["mention_keys"]; ok {
|
|
// Remove any blank mention keys
|
|
splitKeys := strings.Split(u.NotifyProps["mention_keys"], ",")
|
|
goodKeys := []string{}
|
|
for _, key := range splitKeys {
|
|
if len(key) > 0 {
|
|
goodKeys = append(goodKeys, strings.ToLower(key))
|
|
}
|
|
}
|
|
u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",")
|
|
}
|
|
}
|
|
|
|
func (u *User) SetDefaultNotifications() {
|
|
u.NotifyProps = make(map[string]string)
|
|
u.NotifyProps["email"] = "true"
|
|
u.NotifyProps["push"] = USER_NOTIFY_MENTION
|
|
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
|
|
u.NotifyProps["desktop_sound"] = "true"
|
|
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
|
|
u.NotifyProps["channel"] = "true"
|
|
|
|
if u.FirstName == "" {
|
|
u.NotifyProps["first_name"] = "false"
|
|
} else {
|
|
u.NotifyProps["first_name"] = "true"
|
|
}
|
|
}
|
|
|
|
func (user *User) UpdateMentionKeysFromUsername(oldUsername string) {
|
|
nonUsernameKeys := []string{}
|
|
splitKeys := strings.Split(user.NotifyProps["mention_keys"], ",")
|
|
for _, key := range splitKeys {
|
|
if key != oldUsername && key != "@"+oldUsername {
|
|
nonUsernameKeys = append(nonUsernameKeys, key)
|
|
}
|
|
}
|
|
|
|
user.NotifyProps["mention_keys"] = user.Username + ",@" + user.Username
|
|
if len(nonUsernameKeys) > 0 {
|
|
user.NotifyProps["mention_keys"] += "," + strings.Join(nonUsernameKeys, ",")
|
|
}
|
|
}
|
|
|
|
// ToJson convert a User to a json string
|
|
func (u *User) ToJson() string {
|
|
b, err := json.Marshal(u)
|
|
if err != nil {
|
|
return ""
|
|
} else {
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
// Generate a valid strong etag so the browser can cache the results
|
|
func (u *User) Etag(showFullName, showEmail bool) string {
|
|
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
|
|
}
|
|
|
|
// Remove any private data from the user object
|
|
func (u *User) Sanitize(options map[string]bool) {
|
|
u.Password = ""
|
|
u.AuthData = new(string)
|
|
*u.AuthData = ""
|
|
u.MfaSecret = ""
|
|
|
|
if len(options) != 0 && !options["email"] {
|
|
u.Email = ""
|
|
}
|
|
if len(options) != 0 && !options["fullname"] {
|
|
u.FirstName = ""
|
|
u.LastName = ""
|
|
}
|
|
if len(options) != 0 && !options["passwordupdate"] {
|
|
u.LastPasswordUpdate = 0
|
|
}
|
|
if len(options) != 0 && !options["authservice"] {
|
|
u.AuthService = ""
|
|
}
|
|
}
|
|
|
|
func (u *User) ClearNonProfileFields() {
|
|
u.Password = ""
|
|
u.AuthData = new(string)
|
|
*u.AuthData = ""
|
|
u.MfaActive = false
|
|
u.MfaSecret = ""
|
|
u.EmailVerified = false
|
|
u.AllowMarketing = false
|
|
u.Props = StringMap{}
|
|
u.NotifyProps = StringMap{}
|
|
u.LastPasswordUpdate = 0
|
|
u.LastPictureUpdate = 0
|
|
u.FailedAttempts = 0
|
|
}
|
|
|
|
func (u *User) SanitizeProfile(options map[string]bool) {
|
|
u.ClearNonProfileFields()
|
|
|
|
u.Sanitize(options)
|
|
}
|
|
|
|
func (u *User) MakeNonNil() {
|
|
if u.Props == nil {
|
|
u.Props = make(map[string]string)
|
|
}
|
|
|
|
if u.NotifyProps == nil {
|
|
u.NotifyProps = make(map[string]string)
|
|
}
|
|
}
|
|
|
|
func (u *User) AddProp(key string, value string) {
|
|
u.MakeNonNil()
|
|
|
|
u.Props[key] = value
|
|
}
|
|
|
|
func (u *User) AddNotifyProp(key string, value string) {
|
|
u.MakeNonNil()
|
|
|
|
u.NotifyProps[key] = value
|
|
}
|
|
|
|
func (u *User) GetFullName() string {
|
|
if u.FirstName != "" && u.LastName != "" {
|
|
return u.FirstName + " " + u.LastName
|
|
} else if u.FirstName != "" {
|
|
return u.FirstName
|
|
} else if u.LastName != "" {
|
|
return u.LastName
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (u *User) GetDisplayName() string {
|
|
if u.Nickname != "" {
|
|
return u.Nickname
|
|
} else if fullName := u.GetFullName(); fullName != "" {
|
|
return fullName
|
|
} else {
|
|
return u.Username
|
|
}
|
|
}
|
|
|
|
func (u *User) GetDisplayNameForPreference(nameFormat string) string {
|
|
displayName := u.Username
|
|
|
|
if nameFormat == PREFERENCE_VALUE_DISPLAY_NAME_NICKNAME {
|
|
if u.Nickname != "" {
|
|
displayName = u.Nickname
|
|
} else if fullName := u.GetFullName(); fullName != "" {
|
|
displayName = fullName
|
|
}
|
|
} else if nameFormat == PREFERENCE_VALUE_DISPLAY_NAME_FULL {
|
|
if fullName := u.GetFullName(); fullName != "" {
|
|
displayName = fullName
|
|
}
|
|
}
|
|
|
|
return displayName
|
|
}
|
|
|
|
func (u *User) GetRoles() []string {
|
|
return strings.Fields(u.Roles)
|
|
}
|
|
|
|
func (u *User) GetRawRoles() string {
|
|
return u.Roles
|
|
}
|
|
|
|
func IsValidUserRoles(userRoles string) bool {
|
|
|
|
roles := strings.Fields(userRoles)
|
|
|
|
for _, r := range roles {
|
|
if !isValidRole(r) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isValidRole(roleId string) bool {
|
|
_, ok := BuiltInRoles[roleId]
|
|
return ok
|
|
}
|
|
|
|
// Make sure you acually want to use this function. In context.go there are functions to check permissions
|
|
// This function should not be used to check permissions.
|
|
func (u *User) IsInRole(inRole string) bool {
|
|
return IsInRole(u.Roles, inRole)
|
|
}
|
|
|
|
// Make sure you acually want to use this function. In context.go there are functions to check permissions
|
|
// This function should not be used to check permissions.
|
|
func IsInRole(userRoles string, inRole string) bool {
|
|
roles := strings.Split(userRoles, " ")
|
|
|
|
for _, r := range roles {
|
|
if r == inRole {
|
|
return true
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (u *User) IsOAuthUser() bool {
|
|
if u.AuthService == USER_AUTH_SERVICE_GITLAB {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (u *User) IsLDAPUser() bool {
|
|
if u.AuthService == USER_AUTH_SERVICE_LDAP {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UserFromJson will decode the input and return a User
|
|
func UserFromJson(data io.Reader) *User {
|
|
decoder := json.NewDecoder(data)
|
|
var user User
|
|
err := decoder.Decode(&user)
|
|
if err == nil {
|
|
return &user
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func UserMapToJson(u map[string]*User) string {
|
|
b, err := json.Marshal(u)
|
|
if err != nil {
|
|
return ""
|
|
} else {
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
func UserMapFromJson(data io.Reader) map[string]*User {
|
|
decoder := json.NewDecoder(data)
|
|
var users map[string]*User
|
|
err := decoder.Decode(&users)
|
|
if err == nil {
|
|
return users
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func UserListToJson(u []*User) string {
|
|
b, err := json.Marshal(u)
|
|
if err != nil {
|
|
return ""
|
|
} else {
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
func UserListFromJson(data io.Reader) []*User {
|
|
decoder := json.NewDecoder(data)
|
|
var users []*User
|
|
err := decoder.Decode(&users)
|
|
if err == nil {
|
|
return users
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
|
|
func HashPassword(password string) string {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return string(hash)
|
|
}
|
|
|
|
// ComparePassword compares the hash
|
|
func ComparePassword(hash string, password string) bool {
|
|
|
|
if len(password) == 0 || len(hash) == 0 {
|
|
return false
|
|
}
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
return err == nil
|
|
}
|
|
|
|
var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
|
|
|
|
var restrictedUsernames = []string{
|
|
"all",
|
|
"channel",
|
|
"matterbot",
|
|
}
|
|
|
|
func IsValidUsername(s string) bool {
|
|
if len(s) == 0 || len(s) > 64 {
|
|
return false
|
|
}
|
|
|
|
if !validUsernameChars.MatchString(s) {
|
|
return false
|
|
}
|
|
|
|
for _, restrictedUsername := range restrictedUsernames {
|
|
if s == restrictedUsername {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func CleanUsername(s string) string {
|
|
s = strings.ToLower(strings.Replace(s, " ", "-", -1))
|
|
|
|
for _, value := range reservedName {
|
|
if s == value {
|
|
s = strings.Replace(s, value, "", -1)
|
|
}
|
|
}
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
for _, c := range s {
|
|
char := fmt.Sprintf("%c", c)
|
|
if !validUsernameChars.MatchString(char) {
|
|
s = strings.Replace(s, char, "-", -1)
|
|
}
|
|
}
|
|
|
|
s = strings.Trim(s, "-")
|
|
|
|
if !IsValidUsername(s) {
|
|
s = "a" + NewId()
|
|
}
|
|
|
|
return s
|
|
}
|