Files
mostlymatter/app/user.go
Agniva De Sarker 021c90f29f MM-30882: Fix read-after-write issue for demoting user (#16911)
* MM-30882: Fix read-after-write issue for demoting user

In (*App).DemoteUserToGuest, we would demote a user, and then immediately
read it back to do future operations from the user. This reading back
of the user had the effect of sticking the old value into the cache
after which it would never be updated.

There was another issue along with this, which was when the invalidation
message would broadcast across the cluster, it would hit the cache invalidation
problem where an unrelated store call would miss the cache because
it was invalidated, and then again read from replica and stick the old value.

To fix all these, we return the new value directly from the store method
to avoid having the app to read it again.

And we add a map in the localcache layer which tracks invalidations made,
and then switch to use master if it's true.

The core change is fairly limited, but due to changing the store method signatures,
a lot of code needed to be updated to pass "context.Background". Therefore the PR
just "appears" to be big, but the main changes are limited to app/user.go,
sqlstore/user_store.go and user_layer.go

https://mattermost.atlassian.net/browse/MM-30882

```release-note
Fix an issue where demoting a user to guest would not take effect in
an environment with read replicas.
```

* Fix concurrent map access

* Fixing mistakes

* fix tests
2021-02-12 19:04:05 +05:30

2437 строки
81 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
b64 "encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"image"
"image/color"
"image/draw"
_ "image/gif"
_ "image/jpeg"
"image/png"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/disintegration/imaging"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/mattermost/mattermost-server/v5/services/mfa"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
)
const (
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
PasswordRecoverExpiryTime = 1000 * 60 * 60 // 1 hour
InvitationExpiryTime = 1000 * 60 * 60 * 48 // 48 hours
ImageProfilePixelDimension = 128
)
func (a *App) CreateUserWithToken(user *model.User, token *model.Token) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJson(strings.NewReader(token.Extra))
team, nErr := a.Srv().Store.Team().Get(tokenData["teamId"])
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.find.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.finding.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
channels, nErr := a.Srv().Store.Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " "), false)
if nErr != nil {
return nil, model.NewAppError("CreateUserWithToken", "app.channel.get_channels_by_ids.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
user.Email = tokenData["email"]
user.EmailVerified = true
var ruser *model.User
var err *model.AppError
if token.Type == TokenTypeTeamInvitation {
ruser, err = a.CreateUser(user)
} else {
ruser, err = a.CreateGuest(user)
}
if err != nil {
return nil, err
}
if err := a.JoinUserToTeam(team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(team.Id, ruser)
if token.Type == TokenTypeGuestInvitation {
for _, channel := range channels {
_, err := a.AddChannelMember(ruser.Id, channel, "", "")
if err != nil {
mlog.Warn("Failed to add channel member", mlog.Err(err))
}
}
}
if err := a.DeleteToken(token); err != nil {
mlog.Warn("Error while deleting token", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserWithInviteId(user *model.User, inviteId, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
team, nErr := a.Srv().Store.Team().GetByInviteId(inviteId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
if team.IsGroupConstrained() {
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
}
if !CheckUserDomain(user, team.AllowedDomains) {
return nil, model.NewAppError("CreateUserWithInviteId", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": team.AllowedDomains}, "", http.StatusForbidden)
}
user.EmailVerified = false
ruser, err := a.CreateUser(user)
if err != nil {
return nil, err
}
if err := a.JoinUserToTeam(team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(team.Id, ruser)
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserAsAdmin(user *model.User, redirect string) (*model.User, *model.AppError) {
ruser, err := a.CreateUser(user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email on create admin user", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserFromSignup(user *model.User, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if !a.IsFirstUserAccount() && !*a.Config().TeamSettings.EnableOpenServer {
err := model.NewAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email, http.StatusForbidden)
return nil, err
}
user.EmailVerified = false
ruser, err := a.CreateUser(user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
}
return ruser, nil
}
func (a *App) IsUserSignUpAllowed() *model.AppError {
if !*a.Config().EmailSettings.EnableSignUpWithEmail || !*a.Config().TeamSettings.EnableUserCreation {
err := model.NewAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func (s *Server) IsFirstUserAccount() bool {
cachedSessions, err := s.sessionCache.Len()
if err != nil {
return false
}
if cachedSessions == 0 {
count, err := s.Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
mlog.Debug("There was an error fetching if first user account", mlog.Err(err))
return false
}
if count <= 0 {
return true
}
}
return false
}
func (a *App) IsFirstUserAccount() bool {
return a.Srv().IsFirstUserAccount()
}
// CreateUser creates a user and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(user, false)
}
// CreateGuest creates a guest and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateGuest(user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(user, true)
}
func (a *App) createUserOrGuest(user *model.User, guest bool) (*model.User, *model.AppError) {
user.Roles = model.SYSTEM_USER_ROLE_ID
if guest {
user.Roles = model.SYSTEM_GUEST_ROLE_ID
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
// Below is a special case where the first user in the entire
// system is granted the system_admin role
count, err := a.Srv().Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
return nil, model.NewAppError("createUserOrGuest", "app.user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if count <= 0 {
user.Roles = model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID
}
if _, ok := utils.GetSupportedLocales()[user.Locale]; !ok {
user.Locale = *a.Config().LocalizationSettings.DefaultClientLocale
}
ruser, appErr := a.createUser(user)
if appErr != nil {
return nil, appErr
}
// This message goes to everyone, so the teamID, channelId and userID are irrelevant
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
message.Add("user_id", ruser.Id)
a.Publish(message)
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
a.Srv().Go(func() {
pluginContext := a.PluginContext()
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
hooks.UserHasBeenCreated(pluginContext, user)
return true
}, plugin.UserHasBeenCreatedId)
})
}
return ruser, nil
}
func (a *App) createUser(user *model.User) (*model.User, *model.AppError) {
user.MakeNonNil()
if err := a.IsPasswordValid(user.Password); user.AuthService == "" && err != nil {
return nil, err
}
ruser, nErr := a.Srv().Store.User().Save(user)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
switch invErr.Field {
case "email":
return nil, model.NewAppError("createUser", "app.user.save.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest)
case "username":
return nil, model.NewAppError("createUser", "app.user.save.username_exists.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("createUser", "app.user.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
}
default:
return nil, model.NewAppError("createUser", "app.user.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
if user.EmailVerified {
if err := a.VerifyUserEmail(ruser.Id, user.Email); err != nil {
mlog.Warn("Failed to set email verified", mlog.Err(err))
}
}
pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"}
if err := a.Srv().Store.Preference().Save(&model.Preferences{pref}); err != nil {
mlog.Warn("Encountered error saving tutorial preference", mlog.Err(err))
}
go a.UpdateViewedProductNoticesForNewUser(ruser.Id)
ruser.Sanitize(map[string]bool{})
return ruser, nil
}
func (a *App) CreateOAuthUser(service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
if !*a.Config().TeamSettings.EnableUserCreation {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented)
}
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
user, err1 := provider.GetUserFromJson(userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusInternalServerError)
}
if user.AuthService == "" {
user.AuthService = service
}
found := true
count := 0
for found {
if found = a.IsUsernameTaken(user.Username); found {
user.Username = user.Username + strconv.Itoa(count)
count++
}
}
userByAuth, _ := a.Srv().Store.User().GetByAuth(user.AuthData, service)
if userByAuth != nil {
return userByAuth, nil
}
userByEmail, _ := a.Srv().Store.User().GetByEmail(user.Email)
if userByEmail != nil {
if userByEmail.AuthService == "" {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email, http.StatusBadRequest)
}
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]interface{}{"Service": service, "Auth": userByEmail.AuthService}, "email="+user.Email, http.StatusBadRequest)
}
user.EmailVerified = true
ruser, err := a.CreateUser(user)
if err != nil {
return nil, err
}
if teamID != "" {
err = a.AddUserToTeamByTeamId(teamID, user)
if err != nil {
return nil, err
}
err = a.AddDirectChannels(teamID, user)
if err != nil {
mlog.Warn("Failed to add direct channels", mlog.Err(err))
}
}
return ruser, nil
}
// CheckEmailDomain checks that an email domain matches a list of space-delimited domains as a string.
func CheckEmailDomain(email string, domains string) bool {
if domains == "" {
return true
}
domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
for _, d := range domainArray {
if strings.HasSuffix(strings.ToLower(email), "@"+d) {
return true
}
}
return false
}
// CheckUserDomain checks that a user's email domain matches a list of space-delimited domains as a string.
func CheckUserDomain(user *model.User, domains string) bool {
return CheckEmailDomain(user.Email, domains)
}
// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid.
func (a *App) IsUsernameTaken(name string) bool {
if !model.IsValidUsername(name) {
return false
}
if _, err := a.Srv().Store.User().GetByUsername(name); err != nil {
return false
}
return true
}
func (a *App) GetUser(userID string) (*model.User, *model.AppError) {
user, err := a.Srv().Store.User().Get(context.Background(), userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUser", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetUser", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return user, nil
}
func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) {
result, err := a.Srv().Store.User().GetByUsername(username)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return result, nil
}
func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) {
user, err := a.Srv().Store.User().GetByEmail(email)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, err.Error(), http.StatusInternalServerError)
}
}
return user, nil
}
func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
user, err := a.Srv().Store.User().GetByAuth(authData, authService)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, invErr.Error(), http.StatusBadRequest)
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, nfErr.Error(), http.StatusInternalServerError)
default:
return nil, model.NewAppError("GetUserByAuth", "app.user.get_by_auth.other.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return user, nil
}
func (a *App) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetAllProfiles(options)
if err != nil {
return nil, model.NewAppError("GetUsers", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsers(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersEtag(restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForAllProfiles(), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfiles(options)
if err != nil {
return nil, model.NewAppError("GetUsersInTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInTeam(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForProfiles(teamID), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (a *App) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForProfilesNotInTeam(teamID), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (a *App) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesInChannel(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannel", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesInChannelByStatus(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannelByStatus", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannelByStatus(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInChannel", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersNotInChannelMap(teamID string, channelId string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersNotInChannelPage(teamID string, channelId string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelId, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersWithoutTeam(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesWithoutTeam(options)
if err != nil {
return nil, model.NewAppError("GetUsersWithoutTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
// GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers.
func (a *App) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetTeamGroupUsers(teamID)
if err != nil {
return nil, model.NewAppError("GetTeamGroupUsers", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
// GetChannelGroupUsers returns the users who are associated to the channel via GroupChannels and GroupMembers.
func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetChannelGroupUsers(channelID)
if err != nil {
return nil, model.NewAppError("GetChannelGroupUsers", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
func (a *App) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
allowFromCache := options.ViewRestrictions == nil
users, err := a.Srv().Store.User().GetProfileByIds(context.Background(), userIDs, options, allowFromCache)
if err != nil {
return nil, model.NewAppError("GetUsersByIds", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return a.sanitizeProfiles(users, options.IsAdmin), nil
}
func (a *App) GetUsersByGroupChannelIds(channelIds []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
usersByChannelId, err := a.Srv().Store.User().GetProfileByGroupChannelIdsForUser(a.Session().UserId, channelIds)
if err != nil {
return nil, model.NewAppError("GetUsersByGroupChannelIds", "app.user.get_profile_by_group_channel_ids_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for channelId, userList := range usersByChannelId {
usersByChannelId[channelId] = a.sanitizeProfiles(userList, asAdmin)
}
return usersByChannelId, nil
}
func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store.User().GetProfilesByUsernames(usernames, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersByUsernames", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
for _, u := range users {
a.SanitizeProfile(u, asAdmin)
}
return users
}
func (a *App) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
mfaService := mfa.New(a, a.Srv().Store)
secret, img, err := mfaService.GenerateSecret(user)
if err != nil {
return nil, err
}
// Make sure the old secret is not cached on any cluster nodes.
a.InvalidateCacheForUser(user.Id)
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: b64.StdEncoding.EncodeToString(img)}
return mfaSecret, nil
}
func (a *App) ActivateMfa(userID, token string) *model.AppError {
user, err := a.Srv().Store.User().Get(context.Background(), userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("ActivateMfa", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return model.NewAppError("ActivateMfa", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if user.AuthService != "" && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
return model.NewAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "", http.StatusBadRequest)
}
mfaService := mfa.New(a, a.Srv().Store)
if err := mfaService.Activate(user, token); err != nil {
return err
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func (a *App) DeactivateMfa(userID string) *model.AppError {
mfaService := mfa.New(a, a.Srv().Store)
if err := mfaService.Deactivate(userID); err != nil {
return err
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func CreateProfileImage(username string, userID string, initialFont 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])
font, err := getFont(initialFont)
if err != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError)
}
color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, ImageProfilePixelDimension, ImageProfilePixelDimension))
srcImg := image.White
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
size := float64(ImageProfilePixelDimension / 2)
c := freetype.NewContext()
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(dstImg.Bounds())
c.SetDst(dstImg)
c.SetSrc(srcImg)
pt := freetype.Pt(ImageProfilePixelDimension/5, ImageProfilePixelDimension*2/3)
_, err = c.DrawString(initial, pt)
if err != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error(), http.StatusInternalServerError)
}
buf := new(bytes.Buffer)
if imgErr := png.Encode(buf, dstImg); imgErr != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error(), http.StatusInternalServerError)
}
return buf.Bytes(), nil
}
func getFont(initialFont string) (*truetype.Font, error) {
// Some people have the old default font still set, so just treat that as if they're using the new default
if initialFont == "luximbi.ttf" {
initialFont = "nunito-bold.ttf"
}
fontDir, _ := fileutils.FindDir("fonts")
fontBytes, err := ioutil.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, err
}
return freetype.ParseFont(fontBytes)
}
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
if *a.Config().FileSettings.DriverName == "" {
img, appErr := a.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
return img, false, nil
}
path := "users/" + user.Id + "/profile.png"
data, err := a.ReadFile(path)
if err != nil {
img, appErr := a.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (a *App) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
var img []byte
var appErr *model.AppError
if user.IsBot {
img = model.BotDefaultImage
appErr = nil
} else {
img, appErr = CreateProfileImage(user.Username, user.Id, *a.Config().FileSettings.InitialFont)
}
if appErr != nil {
return nil, appErr
}
return img, nil
}
func (a *App) SetDefaultProfileImage(user *model.User) *model.AppError {
img, appErr := a.GetDefaultProfileImage(user)
if appErr != nil {
return appErr
}
path := "users/" + user.Id + "/profile.png"
if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
return err
}
if err := a.Srv().Store.User().ResetLastPictureUpdate(user.Id); err != nil {
mlog.Warn("Failed to reset last picture update", mlog.Err(err))
}
a.InvalidateCacheForUser(user.Id)
updatedUser, appErr := a.GetUser(user.Id)
if appErr != nil {
mlog.Warn("Error in getting users profile forcing logout", mlog.String("user_id", user.Id), mlog.Err(appErr))
return nil
}
options := a.Config().GetSanitizeOptions()
updatedUser.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
message.Add("user", updatedUser)
a.Publish(message)
return nil
}
func (a *App) SetProfileImage(userID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error(), http.StatusBadRequest)
}
defer file.Close()
return a.SetProfileImageFromMultiPartFile(userID, file)
}
func (a *App) SetProfileImageFromMultiPartFile(userID string, file multipart.File) *model.AppError {
// Decode image config first to check dimensions before loading the whole thing into memory later on
config, _, err := image.DecodeConfig(file)
if err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
}
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
if int64(config.Width)*int64(config.Height) > model.MaxImageSize {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusBadRequest)
}
file.Seek(0, 0)
return a.SetProfileImageFromFile(userID, file)
}
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
// Decode image into Image object
img, _, err := image.Decode(file)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest)
}
orientation, _ := getImageOrientation(file)
img = makeImageUpright(img, orientation)
// Scale profile image
profileWidthAndHeight := 128
img = imaging.Fill(img, profileWidthAndHeight, profileWidthAndHeight, imaging.Center, imaging.Lanczos)
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return buf, nil
}
func (a *App) SetProfileImageFromFile(userID string, file io.Reader) *model.AppError {
buf, err := a.AdjustImage(file)
if err != nil {
return err
}
path := "users/" + userID + "/profile.png"
if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "", http.StatusInternalServerError)
}
if err := a.Srv().Store.User().UpdateLastPictureUpdate(userID); err != nil {
mlog.Warn("Error with updating last picture update", mlog.Err(err))
}
a.invalidateUserCacheAndPublish(userID)
return nil
}
func (a *App) UpdatePasswordAsUser(userID, currentPassword, newPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if user == nil {
err = model.NewAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "", http.StatusBadRequest)
return err
}
if user.AuthData != nil && *user.AuthData != "" {
err = model.NewAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService, http.StatusBadRequest)
return err
}
if err := a.DoubleCheckPassword(user, currentPassword); err != nil {
if err.Id == "api.user.check_user_password.invalid.app_error" {
err = model.NewAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "", http.StatusBadRequest)
}
return err
}
T := utils.GetUserTranslations(user.Locale)
return a.UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu"))
}
func (a *App) userDeactivated(userID string) *model.AppError {
if err := a.RevokeAllSessions(userID); err != nil {
return err
}
a.SetStatusOffline(userID, false)
user, err := a.GetUser(userID)
if err != nil {
return err
}
// when disable a user, userDeactivated is called for the user and the
// bots the user owns. Only notify once, when the user is the owner, not the
// owners bots
if !user.IsBot {
a.notifySysadminsBotOwnerDeactivated(userID)
}
if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated {
a.disableUserBots(userID)
}
return nil
}
func (a *App) invalidateUserChannelMembersCaches(userID string) *model.AppError {
teamsForUser, err := a.GetTeamsForUser(userID)
if err != nil {
return err
}
for _, team := range teamsForUser {
channelsForUser, err := a.GetChannelsForUser(team.Id, userID, false, 0)
if err != nil {
return err
}
for _, channel := range *channelsForUser {
a.invalidateCacheForChannelMembers(channel.Id)
}
}
return nil
}
func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) {
user.UpdateAt = model.GetMillis()
if active {
user.DeleteAt = 0
} else {
user.DeleteAt = user.UpdateAt
}
userUpdate, err := a.Srv().Store.User().Update(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateActive", "app.user.update.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("UpdateActive", "app.user.update.finding.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
ruser := userUpdate.New
if !active {
if err := a.userDeactivated(ruser.Id); err != nil {
return nil, err
}
}
a.invalidateUserChannelMembersCaches(user.Id)
a.InvalidateCacheForUser(user.Id)
a.sendUpdatedUserEvent(*ruser)
return ruser, nil
}
func (a *App) DeactivateGuests() *model.AppError {
userIDs, err := a.Srv().Store.User().DeactivateGuests()
if err != nil {
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, userID := range userIDs {
if err := a.userDeactivated(userID); err != nil {
return err
}
}
a.Srv().Store.Channel().ClearCaches()
a.Srv().Store.User().ClearCaches()
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_GUESTS_DEACTIVATED, "", "", "", nil)
a.Publish(message)
return nil
}
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
options := a.Config().GetSanitizeOptions()
if asAdmin {
options["email"] = true
options["fullname"] = true
options["authservice"] = true
}
return options
}
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
options := a.GetSanitizeOptions(asAdmin)
user.SanitizeProfile(options)
}
func (a *App) UpdateUserAsUser(user *model.User, asAdmin bool) (*model.User, *model.AppError) {
updatedUser, err := a.UpdateUser(user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
func (a *App) PatchUser(userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
user.Patch(patch)
updatedUser, err := a.UpdateUser(user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
func (a *App) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
userAuth.Password = ""
if _, err := a.Srv().Store.User().UpdateAuthData(userID, userAuth.AuthService, userAuth.AuthData, "", false); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return userAuth, nil
}
func (a *App) sendUpdatedUserEvent(user model.User) {
adminCopyOfUser := user.DeepCopy()
a.SanitizeProfile(adminCopyOfUser, true)
adminMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
adminMessage.Add("user", adminCopyOfUser)
adminMessage.GetBroadcast().ContainsSensitiveData = true
a.Publish(adminMessage)
a.SanitizeProfile(&user, false)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
message.Add("user", &user)
message.GetBroadcast().ContainsSanitizedData = true
a.Publish(message)
}
func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
prev, err := a.Srv().Store.User().Get(context.Background(), user.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateUser", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("UpdateUser", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
var newEmail string
if user.Email != prev.Email {
if !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
if !prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if !CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
if prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_guest_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if _, appErr := a.GetUserByEmail(user.Email); appErr == nil {
return nil, model.NewAppError("UpdateUser", "store.sql_user.update.email_taken.app_error", nil, "user_id="+user.Id, http.StatusBadRequest)
}
// Don't set new eMail on user account if email verification is required, this will be done as a post-verification action
// to avoid users being able to set non-controlled eMails as their account email
if *a.Config().EmailSettings.RequireEmailVerification {
newEmail = user.Email
// When a bot is created, prev.Email will be an autogenerated faked email,
// which will not match a CLI email input during bot to user conversions.
// To update a bot users email, do not set the email to the faked email
// stored in prev.Email. Allow using the email defined in the CLI
if !user.IsBot {
user.Email = prev.Email
}
}
}
userUpdate, err := a.Srv().Store.User().Update(user, false)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if sendNotifications {
if userUpdate.New.Email != userUpdate.Old.Email || newEmail != "" {
if *a.Config().EmailSettings.RequireEmailVerification {
a.Srv().Go(func() {
if err := a.SendEmailVerification(userUpdate.New, newEmail, ""); err != nil {
mlog.Error("Failed to send email verification", mlog.Err(err))
}
})
} else {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
}
}
if userUpdate.New.Username != userUpdate.Old.Username {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send change username email", mlog.Err(err))
}
})
}
a.sendUpdatedUserEvent(*userUpdate.New)
}
a.InvalidateCacheForUser(user.Id)
return userUpdate.New, nil
}
func (a *App) UpdateUserActive(userID string, active bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if _, err = a.UpdateActive(user, active); err != nil {
return err
}
return nil
}
func (a *App) UpdateUserNotifyProps(userID string, props map[string]string, sendNotifications bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
user.NotifyProps = props
ruser, err := a.UpdateUser(user, sendNotifications)
if err != nil {
return nil, err
}
return ruser, nil
}
func (a *App) UpdateMfa(activate bool, userID, token string) *model.AppError {
if activate {
if err := a.ActivateMfa(userID, token); err != nil {
return err
}
} else {
if err := a.DeactivateMfa(userID); err != nil {
return err
}
}
a.Srv().Go(func() {
user, err := a.GetUser(userID)
if err != nil {
mlog.Error("Failed to get user", mlog.Err(err))
return
}
if err := a.Srv().EmailService.sendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send mfa change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdatePasswordByUserIdSendEmail(userID, newPassword, method string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdatePasswordSendEmail(user, newPassword, method)
}
func (a *App) UpdatePassword(user *model.User, newPassword string) *model.AppError {
if err := a.IsPasswordValid(newPassword); err != nil {
return err
}
hashedPassword := model.HashPassword(newPassword)
if err := a.Srv().Store.User().UpdatePassword(user.Id, hashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, err.Error(), http.StatusInternalServerError)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method string) *model.AppError {
if err := a.UpdatePassword(user, newPassword); err != nil {
return err
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send password change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdateHashedPasswordByUserId(userID, newHashedPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdateHashedPassword(user, newHashedPassword)
}
func (a *App) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
if err := a.Srv().Store.User().UpdatePassword(user.Id, newHashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, err.Error(), http.StatusInternalServerError)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) ResetPasswordFromToken(userSuppliedTokenString, newPassword string) *model.AppError {
token, err := a.GetPasswordRecoveryToken(userSuppliedTokenString)
if err != nil {
return err
}
if model.GetMillis()-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("resetPassword", "api.user.reset_password.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
if user.Email != tokenData.Email {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
if user.IsSSOUser() {
return model.NewAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
T := utils.GetUserTranslations(user.Locale)
if err := a.UpdatePasswordSendEmail(user, newPassword, T("api.user.reset_password.method")); err != nil {
return err
}
if err := a.DeleteToken(token); err != nil {
mlog.Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
user, err := a.GetUserByEmail(email)
if err != nil {
return false, nil
}
if user.AuthData != nil && *user.AuthData != "" {
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
token, err := a.CreatePasswordRecoveryToken(user.Id, user.Email)
if err != nil {
return false, err
}
return a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
}
func (a *App) CreatePasswordRecoveryToken(userID, email string) (*model.Token, *model.AppError) {
tokenExtra := struct {
UserId string
Email string
}{
userID,
email,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, model.NewAppError("CreatePasswordRecoveryToken", "api.user.create_password_token.error", nil, "", http.StatusInternalServerError)
}
token := model.NewToken(TokenTypePasswordRecovery, string(jsonData))
if err := a.Srv().Store.Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreatePasswordRecoveryToken", "app.recover.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return token, nil
}
func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store.Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.invalid_link.app_error", nil, err.Error(), http.StatusBadRequest)
}
if rtoken.Type != TokenTypePasswordRecovery {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
func (a *App) DeleteToken(token *model.Token) *model.AppError {
err := a.Srv().Store.Token().Delete(token.Token)
if err != nil {
return model.NewAppError("DeleteToken", "app.recover.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) UpdateUserRoles(userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
return a.UpdateUserRolesWithUser(user, newRoles, sendWebSocketEvent)
}
func (a *App) UpdateUserRolesWithUser(user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
return nil, err
}
user.Roles = newRoles
uchan := make(chan store.StoreResult, 1)
go func() {
userUpdate, err := a.Srv().Store.User().Update(user, true)
uchan <- store.StoreResult{Data: userUpdate, NErr: err}
close(uchan)
}()
schan := make(chan store.StoreResult, 1)
go func() {
id, err := a.Srv().Store.Session().UpdateRoles(user.Id, newRoles)
schan <- store.StoreResult{Data: id, NErr: err}
close(schan)
}()
result := <-uchan
if result.NErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(result.NErr, &appErr):
return nil, appErr
case errors.As(result.NErr, &invErr):
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.finding.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
}
}
ruser := result.Data.(*model.UserUpdate).New
if result := <-schan; result.NErr != nil {
// soft error since the user roles were still updated
mlog.Warn("Failed during updating user roles", mlog.Err(result.NErr))
}
a.InvalidateCacheForUser(user.Id)
a.ClearSessionCacheForUser(user.Id)
if sendWebSocketEvent {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ROLE_UPDATED, "", "", user.Id, nil)
message.Add("user_id", user.Id)
message.Add("roles", newRoles)
a.Publish(message)
}
return ruser, nil
}
func (a *App) PermanentDeleteUser(user *model.User) *model.AppError {
mlog.Warn("Attempting to permanently delete account", mlog.String("user_id", user.Id), mlog.String("user_email", user.Email))
if user.IsInRole(model.SYSTEM_ADMIN_ROLE_ID) {
mlog.Warn("You are deleting a user that is a system administrator. You may need to set another account as the system administrator using the command line tools.", mlog.String("user_email", user.Email))
}
if _, err := a.UpdateActive(user, false); err != nil {
return err
}
if err := a.Srv().Store.Session().PermanentDeleteSessionsByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.session.permanent_delete_sessions_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.UserAccessToken().DeleteAllForUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.oauth.permanent_delete_auth_data_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Webhook().PermanentDeleteIncomingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_incoming_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_outgoing_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Command().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanentdeleteuser.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Preference().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.preference.permanent_delete_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Channel().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.channel.permanent_delete_members_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Group().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.group.permanent_delete_members_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Post().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Bot().PermanentDelete(user.Id); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteUser", "app.bot.permenent_delete.bad_id", map[string]interface{}{"user_id": invErr.Value}, invErr.Error(), http.StatusBadRequest)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("PermanentDeleteUser", "app.bot.permanent_delete.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
infos, err := a.Srv().Store.FileInfo().GetForUser(user.Id)
if err != nil {
mlog.Warn("Error getting file list for user from FileInfoStore", mlog.Err(err))
}
for _, info := range infos {
res, err := a.FileExists(info.Path)
if err != nil {
mlog.Warn(
"Error checking existence of file",
mlog.String("path", info.Path),
mlog.Err(err),
)
continue
}
if !res {
mlog.Warn("File not found", mlog.String("path", info.Path))
continue
}
err = a.RemoveFile(info.Path)
if err != nil {
mlog.Warn(
"Unable to remove file",
mlog.String("path", info.Path),
mlog.Err(err),
)
}
}
if _, err := a.Srv().Store.FileInfo().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, ""+err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.User().PermanentDelete(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanent_delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Audit().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.audit.permanent_delete_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := a.Srv().Store.Team().RemoveAllMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.team.remove_member.app_error", nil, err.Error(), http.StatusInternalServerError)
}
mlog.Warn("Permanently deleted account", mlog.String("user_email", user.Email), mlog.String("user_id", user.Id))
return nil
}
func (a *App) PermanentDeleteAllUsers() *model.AppError {
users, err := a.Srv().Store.User().GetAll()
if err != nil {
return model.NewAppError("PermanentDeleteAllUsers", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.PermanentDeleteUser(user)
}
return nil
}
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
if err != nil {
return err
}
if _, err := a.GetStatus(user.Id); err != nil {
return a.Srv().EmailService.sendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
}
return a.Srv().EmailService.sendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token)
}
func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError {
token, err := a.GetVerifyEmailToken(userSuppliedTokenString)
if err != nil {
return err
}
if model.GetMillis()-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
tokenData.Email = strings.ToLower(tokenData.Email)
if err := a.VerifyUserEmail(tokenData.UserId, tokenData.Email); err != nil {
return err
}
if user.Email != tokenData.Email {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
}
if err := a.DeleteToken(token); err != nil {
mlog.Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store.Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.bad_link.app_error", nil, err.Error(), http.StatusBadRequest)
}
if rtoken.Type != TokenTypeVerifyEmail {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
// GetTotalUsersStats is used for the DM list total
func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store.User().Count(model.UserCountOptions{
IncludeBotAccounts: true,
ViewRestrictions: viewRestrictions,
})
if err != nil {
return nil, model.NewAppError("GetTotalUsersStats", "app.user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
// GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions.
func (a *App) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store.User().Count(*options)
if err != nil {
return nil, model.NewAppError("GetFilteredUsersStats", "app.user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
if _, err := a.Srv().Store.User().VerifyEmail(userID, email); err != nil {
return model.NewAppError("VerifyUserEmail", "app.user.verify_email.app_error", nil, err.Error(), http.StatusInternalServerError)
}
a.InvalidateCacheForUser(userID)
user, err := a.GetUser(userID)
if err != nil {
return err
}
a.sendUpdatedUserEvent(*user)
return nil
}
func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
if props.WithoutTeam {
return a.SearchUsersWithoutTeam(props.Term, options)
}
if props.InChannelId != "" {
return a.SearchUsersInChannel(props.InChannelId, props.Term, options)
}
if props.NotInChannelId != "" {
return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, options)
}
if props.NotInTeamId != "" {
return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, options)
}
if props.InGroupId != "" {
return a.SearchUsersInGroup(props.InGroupId, props.Term, options)
}
return a.SearchUsersInTeam(props.TeamId, props.Term, options)
}
func (a *App) SearchUsersInChannel(channelId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().SearchInChannel(channelId, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInChannel", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInChannel(teamID string, channelId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().SearchNotInChannel(teamID, channelId, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInChannel", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInTeam(teamID, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInTeam", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().SearchNotInTeam(notInTeamId, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInTeam", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().SearchWithoutTeam(term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersWithoutTeam", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().SearchInGroup(groupID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInGroup", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) AutocompleteUsersInChannel(teamID string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
term = strings.TrimSpace(term)
autocomplete, err := a.Srv().Store.User().AutocompleteUsersInChannel(teamID, channelId, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInChannel", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range autocomplete.InChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
for _, user := range autocomplete.OutOfChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
return autocomplete, nil
}
func (a *App) AutocompleteUsersInTeam(teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store.User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInTeam", "app.user.search.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
autocomplete := &model.UserAutocompleteInTeam{}
autocomplete.InTeam = users
return autocomplete, nil
}
func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string, tokenUser *model.User) *model.AppError {
oauthUser, err1 := provider.GetUserFromJson(userData, tokenUser)
if err1 != nil {
return model.NewAppError("UpdateOAuthUserAttrs", "api.user.update_oauth_user_attrs.get_user.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusBadRequest)
}
userAttrsChanged := false
if oauthUser.Username != user.Username {
if existingUser, _ := a.GetUserByUsername(oauthUser.Username); existingUser == nil {
user.Username = oauthUser.Username
userAttrsChanged = true
}
}
if oauthUser.GetFullName() != user.GetFullName() {
user.FirstName = oauthUser.FirstName
user.LastName = oauthUser.LastName
userAttrsChanged = true
}
if oauthUser.Email != user.Email {
if existingUser, _ := a.GetUserByEmail(oauthUser.Email); existingUser == nil {
user.Email = oauthUser.Email
userAttrsChanged = true
}
}
if user.DeleteAt > 0 {
// Make sure they are not disabled
user.DeleteAt = 0
userAttrsChanged = true
}
if userAttrsChanged {
users, err := a.Srv().Store.User().Update(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.find.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.finding.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
user = users.New
a.InvalidateCacheForUser(user.Id)
}
return nil
}
func (a *App) RestrictUsersGetByPermissions(userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
// FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the team excluding bots.
func (a *App) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
teamGroupUsers, err := a.GetTeamGroupUsers(team.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, teamGroupUsers)
}
// FilterNonGroupChannelMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the channel excluding bots
func (a *App) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
channelGroupUsers, err := a.GetChannelGroupUsers(channel.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, channelGroupUsers)
}
// filterNonGroupUsers is a helper function that takes a list of user ids and a list of users
// and returns the list of normal users present in userIDs but not in groupUsers.
func (a *App) filterNonGroupUsers(userIDs []string, groupUsers []*model.User) ([]string, error) {
nonMemberIds := []string{}
users, err := a.Srv().Store.User().GetProfileByIds(context.Background(), userIDs, nil, false)
if err != nil {
return nil, err
}
for _, user := range users {
userIsMember := user.IsBot
for _, pu := range groupUsers {
if pu.Id == user.Id {
userIsMember = true
break
}
}
if !userIsMember {
nonMemberIds = append(nonMemberIds, user.Id)
}
}
return nonMemberIds, nil
}
func (a *App) RestrictUsersSearchByPermissions(userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
func (a *App) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
if userID == otherUserId {
return true, nil
}
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return false, err
}
if restrictions == nil {
return true, nil
}
if len(restrictions.Teams) > 0 {
result, err := a.Srv().Store.Team().UserBelongsToTeams(otherUserId, restrictions.Teams)
if err != nil {
return false, model.NewAppError("UserCanSeeOtherUser", "app.team.user_belongs_to_teams.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if result {
return true, nil
}
}
if len(restrictions.Channels) > 0 {
result, err := a.userBelongsToChannels(otherUserId, restrictions.Channels)
if err != nil {
return false, err
}
if result {
return true, nil
}
}
return false, nil
}
func (a *App) userBelongsToChannels(userID string, channelIds []string) (bool, *model.AppError) {
belongs, err := a.Srv().Store.Channel().UserBelongsToChannels(userID, channelIds)
if err != nil {
return false, model.NewAppError("userBelongsToChannels", "app.channel.user_belongs_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return belongs, nil
}
func (a *App) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError) {
if a.HasPermissionTo(userID, model.PERMISSION_VIEW_MEMBERS) {
return nil, nil
}
teamIDs, nErr := a.Srv().Store.Team().GetUserTeamIds(userID, true)
if nErr != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.team.get_user_team_ids.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
teamIDsWithPermission := []string{}
for _, teamID := range teamIDs {
if a.HasPermissionToTeam(userID, teamID, model.PERMISSION_VIEW_MEMBERS) {
teamIDsWithPermission = append(teamIDsWithPermission, teamID)
}
}
userChannelMembers, err := a.Srv().Store.Channel().GetAllChannelMembersForUser(userID, true, true)
if err != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
channelIds := []string{}
for channelId := range userChannelMembers {
channelIds = append(channelIds, channelId)
}
return &model.ViewUsersRestrictions{Teams: teamIDsWithPermission, Channels: channelIds}, nil
}
// PromoteGuestToUser Convert user's roles and all his mermbership's roles from
// guest roles to regular user roles.
func (a *App) PromoteGuestToUser(user *model.User, requestorId string) *model.AppError {
nErr := a.Srv().Store.User().PromoteGuestToUser(user.Id)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
userTeams, nErr := a.Srv().Store.Team().GetTeamsByUserId(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.team.get_all.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
for _, team := range userTeams {
// Soft error if there is an issue joining the default channels
if err := a.JoinDefaultChannels(team.Id, user, false, requestorId); err != nil {
mlog.Warn("Failed to join default channels", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.String("requestor_id", requestorId), mlog.Err(err))
}
}
promotedUser, err := a.GetUser(user.Id)
if err != nil {
mlog.Warn("Failed to get user on promote guest to user", mlog.Err(err))
} else {
a.sendUpdatedUserEvent(*promotedUser)
a.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest())
}
teamMembers, err := a.GetTeamMembersForUser(user.Id)
if err != nil {
mlog.Warn("Failed to get team members for user on promote guest to user", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, err := a.GetChannelMembersForUser(member.TeamId, user.Id)
if err != nil {
mlog.Warn("Failed to get channel members for user on promote guest to user", mlog.Err(err))
}
for _, member := range *channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", user.Id, nil)
evt.Add("channelMember", member.ToJson())
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
// DemoteUserToGuest Convert user's roles and all his mermbership's roles from
// regular user roles to guest roles.
func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
demotedUser, nErr := a.Srv().Store.User().DemoteUserToGuest(user.Id)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
a.sendUpdatedUserEvent(*demotedUser)
a.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest())
teamMembers, err := a.GetTeamMembersForUser(user.Id)
if err != nil {
mlog.Warn("Failed to get team members for users on demote user to guest", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, err := a.GetChannelMembersForUser(member.TeamId, user.Id)
if err != nil {
mlog.Warn("Failed to get channel members for users on demote user to guest", mlog.Err(err))
continue
}
for _, member := range *channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", user.Id, nil)
evt.Add("channelMember", member.ToJson())
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
func (a *App) PublishUserTyping(userID, channelId, parentId string) *model.AppError {
omitUsers := make(map[string]bool, 1)
omitUsers[userID] = true
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers)
event.Add("parent_id", parentId)
event.Add("user_id", userID)
a.Publish(event)
return nil
}
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
func (a *App) invalidateUserCacheAndPublish(userID string) {
a.InvalidateCacheForUser(userID)
user, userErr := a.GetUser(userID)
if userErr != nil {
mlog.Error("Error in getting users profile", mlog.String("user_id", userID), mlog.Err(userErr))
return
}
options := a.Config().GetSanitizeOptions()
user.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil)
message.Add("user", user)
a.Publish(message)
}
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.
func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) {
users, err := a.Srv().Store.User().GetKnownUsers(userID)
if err != nil {
return nil, model.NewAppError("GetKnownUsers", "app.user.get_known_users.get_users.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
// ConvertBotToUser converts a bot to user.
func (a *App) ConvertBotToUser(bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
user, nErr := a.Srv().Store.User().Get(context.Background(), bot.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("ConvertBotToUser", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("ConvertBotToUser", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
if sysadmin && !user.IsInRole(model.SYSTEM_ADMIN_ROLE_ID) {
_, appErr := a.UpdateUserRoles(
user.Id,
fmt.Sprintf("%s %s", user.Roles, model.SYSTEM_ADMIN_ROLE_ID),
false)
if appErr != nil {
return nil, appErr
}
}
user.Patch(userPatch)
user, err := a.UpdateUser(user, false)
if err != nil {
return nil, err
}
err = a.UpdatePassword(user, *userPatch.Password)
if err != nil {
return nil, err
}
appErr := a.Srv().Store.Bot().PermanentDelete(bot.UserId)
if appErr != nil {
return nil, model.NewAppError("ConvertBotToUser", "app.user.convert_bot_to_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return user, nil
}
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
threads, err := a.Srv().Store.Thread().GetThreadsForUser(userID, teamID, options)
if err != nil {
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, thread := range threads.Threads {
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
}
return threads, nil
}
func (a *App) GetThreadMentionsForUserPerChannel(userId, teamId string) (map[string]int64, *model.AppError) {
res, err := a.Srv().Store.Thread().GetThreadMentionsForUserPerChannel(userId, teamId)
if err != nil {
return nil, model.NewAppError("GetThreadMentionsForUserPerChannel", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return res, nil
}
func (a *App) GetThreadForUser(userID, teamID, threadId string, extended bool) (*model.ThreadResponse, *model.AppError) {
thread, err := a.Srv().Store.Thread().GetThreadForUser(userID, teamID, threadId, extended)
if err != nil {
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if thread == nil {
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound)
}
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
return thread, nil
}
func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
nErr := a.Srv().Store.Thread().MarkAllAsRead(userID, teamID)
if nErr != nil {
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userID, nil)
a.Publish(message)
return nil
}
func (a *App) UpdateThreadFollowForUser(userID, threadId string, state bool) *model.AppError {
err := a.Srv().Store.Thread().CreateMembershipIfNeeded(userID, threadId, state, false, true)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
}
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_FOLLOW_CHANGED, "", "", userID, nil)
message.Add("thread_id", threadId)
message.Add("state", state)
a.Publish(message)
return nil
}
func (a *App) UpdateThreadReadForUser(userID, teamID, threadId string, timestamp int64) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
membership, nErr := a.Srv().Store.Thread().GetMembershipForUser(userID, threadId)
if nErr != nil {
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
post, err := a.GetSinglePost(threadId)
if err != nil {
return err
}
membership.UnreadMentions, err = a.countThreadMentions(user, post, teamID, timestamp)
if err != nil {
return err
}
membership.Following = true
_, nErr = a.Srv().Store.Thread().UpdateMembership(membership)
if nErr != nil {
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
nErr = a.Srv().Store.Thread().MarkAsRead(userID, threadId, timestamp)
if nErr != nil {
return model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_THREAD_READ_CHANGED, "", "", userID, nil)
message.Add("thread_id", threadId)
message.Add("timestamp", timestamp)
a.Publish(message)
return nil
}