mattermost/server/channels/api4/user.go
Miguel de la Cruz 9d06155540
Update bot checks (#36503)
* Fix bot permission checks in revokeSession, revokeAllSessionsForUser, and updatePassword

MM-68701: Align permission checks with the bot-aware pattern used by
updateUser, patchUser, deleteUser, and (via MM-68686) updateUserActive.

Three handlers were missing the IsBot branch:

- revokeSession / revokeAllSessionsForUser: both gated access through
  SessionHasPermissionToUser, which only requires EditOtherUsers (an
  ancillary permission granted to User Managers). Switching to
  SessionHasPermissionToUserOrBot routes bot targets through
  SessionHasPermissionToManageBot first and falls back to the user
  path only when the target is not a bot.

- updatePassword: the permission flag canUpdatePassword was set by
  checking PermissionSysconsoleWriteUserManagementUsers (or
  PermissionManageSystem for system admins) with no IsBot branch.
  Adding an else-if user.IsBot guard routes bot targets through
  SessionHasPermissionToManageBot, consistent with every other
  handler in the file that touches bot accounts.

Co-authored-by: Miguel de la Cruz <mgdelacroix@users.noreply.github.com>

* Improve TestRevokeSessionBotPermissions: revoke a real bot session

Seed a session directly via th.App.CreateSession instead of passing a
fake ID and expecting a 400. The test now validates the full happy path:
the session row is created, the privileged user revokes it, and the
call returns 200 OK.

Co-authored-by: Miguel de la Cruz <mgdelacroix@users.noreply.github.com>

* Strengthen forbidden sub-test: revoke a real bot session with no perms

Seed a real session for the bot before the unprivileged revoke call.
The test now proves the permission gate blocks access even when the
target session ID genuinely exists in the database.

Co-authored-by: Miguel de la Cruz <mgdelacroix@users.noreply.github.com>

* Address review feedback: add post-conditions to bot session revoke tests

- TestRevokeSessionBotPermissions: after RevokeSession succeeds, assert
  GetSessionById returns an error to confirm the row is gone.
- TestRevokeAllSessionsForUserBotPermissions: seed a real session before
  RevokeAllSessions so the call is not a no-op, then assert GetSessions
  returns an empty list afterwards.

Co-authored-by: Miguel de la Cruz <mgdelacroix@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Miguel de la Cruz <mgdelacroix@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-05-18 12:27:38 +02:00

4023 lines
122 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/app/email"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
func (api *API) InitUser() {
api.BaseRoutes.Users.Handle("", api.APIHandler(createUser)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("", api.APISessionRequired(getUsers)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/ids", api.APISessionRequired(getUsersByIds)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/usernames", api.APISessionRequired(getUsersByNames)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/known", api.APISessionRequired(getKnownUsers)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchUsers)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/autocomplete", api.APISessionRequired(autocompleteUsers)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/stats", api.APISessionRequired(getTotalUsersStats)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/stats/filtered", api.APISessionRequired(getFilteredUsersStats)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/group_channels", api.APISessionRequired(getUsersByGroupChannelIds)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage, handlerParamFileAPI)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("", api.APISessionRequired(deleteUser)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/roles", api.APISessionRequired(updateUserRoles)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/active", api.APISessionRequired(updateUserActive)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/password", api.APISessionRequired(updatePassword)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/promote", api.APISessionRequired(promoteGuestToUser)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/demote", api.APISessionRequired(demoteUserToGuest)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/convert_to_bot", api.APISessionRequired(convertUserToBot)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/password/reset", api.APIHandler(resetPassword)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/password/reset/send", api.APIHandler(sendPasswordReset)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/email/verify", api.APIHandler(verifyUserEmail)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/email/verify/send", api.APIHandler(sendVerificationEmail)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/email/verify/member", api.APISessionRequired(verifyUserEmailWithoutToken)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(saveUserTermsOfService)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(getUserTermsOfService)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/reset_failed_attempts", api.APISessionRequired(resetPasswordFailedAttempts)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/auth", api.APISessionRequired(updateUserAuth)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/mfa", api.APISessionRequiredMfa(updateUserMfa)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login", api.RateLimitedHandler(api.APIHandler(login), model.RateLimitSettings{PerSec: new(5), MaxBurst: new(10)})).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/sso/code-exchange", api.APIHandler(loginSSOCodeExchange)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/desktop_token", api.RateLimitedHandler(api.APIHandler(loginWithDesktopToken), model.RateLimitSettings{PerSec: new(2), MaxBurst: new(1)})).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/type", api.APIHandler(getLoginType)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/logout", api.APIHandler(logout)).Methods(http.MethodPost)
api.BaseRoutes.UserByUsername.Handle("", api.APISessionRequired(getUserByUsername)).Methods(http.MethodGet)
api.BaseRoutes.UserByEmail.Handle("", api.APISessionRequired(getUserByEmail)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/auth_data", api.APISessionRequired(getUserByAuthData)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/sessions", api.APISessionRequired(getSessions)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/sessions/revoke", api.APISessionRequired(revokeSession)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsForUser)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsAllUsers)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/sessions/device", api.APISessionRequired(handleDeviceProps)).Methods(http.MethodPut)
api.BaseRoutes.User.Handle("/audits", api.APISessionRequired(getUserAudits)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(createUserAccessToken)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(getUserAccessTokensForUser)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/tokens", api.APISessionRequired(getUserAccessTokens)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/tokens/search", api.APISessionRequired(searchUserAccessTokens)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", api.APISessionRequired(getUserAccessToken)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/tokens/revoke", api.APISessionRequired(revokeUserAccessToken)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/tokens/disable", api.APISessionRequired(disableUserAccessToken)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/tokens/enable", api.APISessionRequired(enableUserAccessToken)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/typing", api.APISessionRequiredDisableWhenBusy(publishUserTyping)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.APISessionRequired(migrateAuthToLDAP)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.APISessionRequired(migrateAuthToSaml)).Methods(http.MethodPost)
api.BaseRoutes.User.Handle("/uploads", api.APISessionRequired(getUploadsForUser)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/channel_members", api.APISessionRequired(getChannelMembersForUser)).Methods(http.MethodGet)
api.BaseRoutes.Users.Handle("/invalid_emails", api.APISessionRequired(getUsersWithInvalidEmails)).Methods(http.MethodGet)
api.BaseRoutes.UserThreads.Handle("", api.APISessionRequired(getThreadsForUser)).Methods(http.MethodGet)
api.BaseRoutes.UserThreads.Handle("/read", api.APISessionRequired(updateReadStateAllThreadsByUser)).Methods(http.MethodPut)
api.BaseRoutes.UserThread.Handle("", api.APISessionRequired(getThreadForUser)).Methods(http.MethodGet)
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(followThreadByUser)).Methods(http.MethodPut)
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods(http.MethodDelete)
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods(http.MethodPut)
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods(http.MethodPost)
}
// loginSSOCodeExchange exchanges a short-lived login_code for session tokens.
//
// Deprecated: This endpoint is deprecated and will be removed in a future release.
// Mobile clients should use the direct SSO callback flow instead.
func loginSSOCodeExchange(c *Context, w http.ResponseWriter, r *http.Request) {
// Set deprecation headers to inform clients
w.Header().Set("Deprecation", "true")
if !c.App.Config().FeatureFlags.MobileSSOCodeExchange {
c.Logger.Warn("Deprecated endpoint called",
mlog.String("endpoint", "/login/sso/code-exchange"),
mlog.String("status", "disabled"),
)
c.Err = model.NewAppError("loginSSOCodeExchange", "api.user.login_sso_code_exchange.deprecated.app_error", nil, "", http.StatusGone)
return
}
c.Logger.Warn("Deprecated endpoint called",
mlog.String("endpoint", "/login/sso/code-exchange"),
mlog.String("status", "enabled but deprecated"),
)
props := model.MapFromJSON(r.Body)
loginCode := props["login_code"]
codeVerifier := props["code_verifier"]
state := props["state"]
if loginCode == "" || codeVerifier == "" || state == "" {
c.SetInvalidParam("login_code | code_verifier | state")
return
}
// Consume one-time code atomically
token, appErr := c.App.ConsumeTokenOnce(model.TokenTypeSSOCodeExchange, loginCode)
if appErr != nil {
c.Err = appErr
return
}
// Check token expiration as fallback to cleanup process
if token.IsExpired() {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "token expired", http.StatusBadRequest)
return
}
// Parse extra JSON
extra := model.MapFromJSON(strings.NewReader(token.Extra))
userID := extra["user_id"]
codeChallenge := extra["code_challenge"]
method := strings.ToUpper(extra["code_challenge_method"])
expectedState := extra["state"]
if userID == "" || codeChallenge == "" || expectedState == "" {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "", http.StatusBadRequest)
return
}
if state != expectedState {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "state mismatch", http.StatusBadRequest)
return
}
// Verify SAML challenge
var computed string
switch strings.ToUpper(method) {
case "S256":
sum := sha256.Sum256([]byte(codeVerifier))
computed = base64.RawURLEncoding.EncodeToString(sum[:])
case "":
computed = codeVerifier
case "PLAIN":
// Explicitly reject plain method for security
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "plain SAML challenge method not supported",
http.StatusBadRequest)
return
default:
// Reject unknown methods
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "unsupported SAML challenge method", http.StatusBadRequest)
return
}
if computed != codeChallenge {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "SAML challenge mismatch", http.StatusBadRequest)
return
}
// Create session for this user
user, err := c.App.GetUser(userID)
if err != nil {
c.Err = err
return
}
isMobile := utils.IsMobileRequest(r)
session, err2 := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
if err2 != nil {
c.Err = err2
return
}
c.AppContext = c.AppContext.WithSession(session)
c.App.AttachSessionCookies(c.AppContext, w, r)
// Respond with tokens for mobile client to set
resp := map[string]string{
"token": session.Token,
"csrf": session.GetCSRF(),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
user.SanitizeInput(c.IsSystemAdmin())
tokenId := r.URL.Query().Get("t")
inviteId := r.URL.Query().Get("iid")
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord(model.AuditEventCreateUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "invite_id", inviteId)
model.AddEventParameterToAuditRec(auditRec, "redirect", redirect)
model.AddEventParameterAuditableToAuditRec(auditRec, "user", &user)
// No permission check required
var ruser *model.User
var err *model.AppError
if tokenId != "" {
token, appErr := c.App.GetTokenById(tokenId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddMeta("token_type", token.Type)
if token.Type == model.TokenTypeGuestInvitation {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.license.app_error", nil, "", http.StatusBadRequest)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.disabled.app_error", nil, "", http.StatusBadRequest)
return
}
}
ruser, err = c.App.CreateUserWithToken(c.AppContext, &user, token)
} else if inviteId != "" {
ruser, err = c.App.CreateUserWithInviteId(c.AppContext, &user, inviteId, redirect)
} else if c.IsSystemAdmin() {
ruser, err = c.App.CreateUserAsAdmin(c.AppContext, &user, redirect)
auditRec.AddMeta("admin", true)
} else {
ruser, err = c.App.CreateUserFromSignup(c.AppContext, &user, redirect)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
auditRec.AddEventObjectType("user")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUsername()
if c.Err != nil {
return
}
user, err := c.App.GetUserByUsername(c.Params.Username)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.SanitizeEmail()
if c.Err != nil {
return
}
sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin())
if !sanitizeOptions["email"] {
c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
user, err := c.App.GetUserByEmail(c.Params.Email)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByAuthData(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
authData := r.URL.Query().Get("value")
if authData == "" {
c.SetInvalidParam("value")
return
}
if len(authData) > model.UserAuthDataMaxLength {
c.SetInvalidParam("value")
return
}
user, err := c.App.GetUserByAuthData(&authData)
if err != nil {
c.Err = err
return
}
canSee, err2 := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, user.Id)
if err2 != nil {
c.Err = err2
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if jerr := json.NewEncoder(w).Encode(user); jerr != nil {
c.Logger.Warn("Error while writing response", mlog.Err(jerr))
}
}
func getDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
img, err := c.App.GetDefaultProfileImage(user)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set("Content-Type", "image/png")
if _, err := w.Write(img); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
etag := strconv.FormatInt(user.LastPictureUpdate, 10)
if c.HandleEtag(etag, "Get Profile Image", w, r) {
return
}
img, readFailed, err := c.App.GetProfileImage(user)
if err != nil {
c.Err = err
return
}
if readFailed {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", 5*60)) // 5 mins
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set(model.HeaderEtagServer, etag)
}
w.Header().Set("Content-Type", "image/png")
if _, err := w.Write(img); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer func() {
if _, err := io.Copy(io.Discard, r.Body); err != nil {
c.Logger.Warn("Error discarding request body", mlog.Err(err))
}
}()
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventSetProfileImage, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
model.AddEventParameterToAuditRec(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidURLParam("user_id")
return
}
auditRec.AddEventResultState(user)
if (user.IsLDAPUser() || (user.IsSAMLUser() && *c.App.Config().SamlSettings.EnableSyncWithLdap)) &&
*c.App.Config().LdapSettings.PictureAttribute != "" {
c.Err = model.NewAppError(
"uploadProfileImage", "api.user.upload_profile_user.login_provider_attribute_set.app_error",
nil, "", http.StatusConflict)
return
}
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func setDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("setDefaultProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventSetDefaultProfileImage, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
if err := c.App.SetDefaultProfileImage(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
restrictions, err := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
stats, err := c.App.GetTotalUsersStats(restrictions)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
teamID := r.URL.Query().Get("in_team")
channelID := r.URL.Query().Get("in_channel")
includeDeleted := r.URL.Query().Get("include_deleted")
includeBotAccounts := r.URL.Query().Get("include_bots")
includeRemoteUsers := r.URL.Query().Get("include_remote_users")
rolesString := r.URL.Query().Get("roles")
channelRolesString := r.URL.Query().Get("channel_roles")
teamRolesString := r.URL.Query().Get("team_roles")
includeDeletedBool, _ := strconv.ParseBool(includeDeleted)
includeBotAccountsBool, _ := strconv.ParseBool(includeBotAccounts)
includeRemoteUsersBool, _ := strconv.ParseBool(includeRemoteUsers)
roles := []string{}
var rolesValid bool
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && channelID != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && teamID != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
}
options := &model.UserCountOptions{
IncludeDeleted: includeDeletedBool,
IncludeBotAccounts: includeBotAccountsBool,
IncludeRemoteUsers: includeRemoteUsersBool,
TeamId: teamID,
ChannelId: channelID,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
stats, err := c.App.GetFilteredUsersStats(options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
channelIds, err := model.SortedArrayFromJSON(r.Body)
if err != nil || len(channelIds) == 0 {
c.Err = model.NewAppError("getUsersByGroupChannelIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
return
} else if len(channelIds) == 0 {
c.SetInvalidParam("channel_ids")
return
}
usersByChannelId, appErr := c.App.GetUsersByGroupChannelIds(c.AppContext, channelIds, c.IsSystemAdmin())
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(usersByChannelId)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var (
query = r.URL.Query()
inTeamId = query.Get("in_team")
notInTeamId = query.Get("not_in_team")
inChannelId = query.Get("in_channel")
inGroupId = query.Get("in_group")
notInGroupId = query.Get("not_in_group")
notInChannelId = query.Get("not_in_channel")
groupConstrained = query.Get("group_constrained")
withoutTeam = query.Get("without_team")
inactive = query.Get("inactive")
active = query.Get("active")
role = query.Get("role")
sort = query.Get("sort")
rolesString = query.Get("roles")
channelRolesString = query.Get("channel_roles")
teamRolesString = query.Get("team_roles")
)
if notInChannelId != "" && inTeamId == "" {
c.SetInvalidURLParam("team_id")
return
}
if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" && sort != "admin" && sort != "display_name" {
c.SetInvalidURLParam("sort")
return
}
// Currently only supports sorting on a team
// or sort="status" on inChannelId
// or sort="display_name" on inGroupId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "" || notInGroupId != "") {
c.SetInvalidURLParam("sort")
return
}
if sort == "status" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "admin" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "display_name" && (inGroupId == "" || notInGroupId != "" || inTeamId != "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidURLParam("sort")
return
}
var (
withoutTeamBool, _ = strconv.ParseBool(withoutTeam)
groupConstrainedBool, _ = strconv.ParseBool(groupConstrained)
inactiveBool, _ = strconv.ParseBool(inactive)
activeBool, _ = strconv.ParseBool(active)
)
if inactiveBool && activeBool {
c.SetInvalidURLParam("inactive")
}
roleNamesAll := []string{}
// MM-47378: validate 'role' related parameters
if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" {
// fetch all role names
rolesAll, err := c.App.GetAllRoles()
if err != nil {
c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest)
return
}
for _, role := range rolesAll {
roleNamesAll = append(roleNamesAll, role.Name)
}
}
roles := []string{}
var rolesValid bool
if role != "" {
roles, rolesValid = model.CleanRoleNames([]string{role})
if !rolesValid {
c.SetInvalidParam("role")
return
}
roleValid := slices.Contains(roleNamesAll, role)
if !roleValid {
c.SetInvalidParam("role")
return
}
}
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles)
if len(validRoleNames) != len(roles) {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && inChannelId != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles)
if len(validRoleNames) != len(channelRoles) {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && inTeamId != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles)
if len(validRoleNames) != len(teamRoles) {
c.SetInvalidParam("teamRoles")
return
}
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
userGetOptions := &model.UserGetOptions{
InTeamId: inTeamId,
InChannelId: inChannelId,
NotInTeamId: notInTeamId,
NotInChannelId: notInChannelId,
InGroupId: inGroupId,
NotInGroupId: notInGroupId,
GroupConstrained: groupConstrainedBool,
WithoutTeam: withoutTeamBool,
Inactive: inactiveBool,
Active: activeBool,
Role: role,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
Sort: sort,
Page: c.Params.Page,
PerPage: c.Params.PerPage,
ViewRestrictions: restrictions,
}
var (
profiles []*model.User
etag string
)
if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
// Use a special permission for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListUsersWithoutTeam) {
c.SetPermissionError(model.PermissionListUsersWithoutTeam)
return
}
profiles, appErr = c.App.GetUsersWithoutTeamPage(userGetOptions, c.IsSystemAdmin())
} else if notInChannelId != "" {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), notInChannelId, model.PermissionReadChannel); !ok {
c.SetPermissionError(model.PermissionReadChannel)
return
}
// ABAC filtering is mandatory for private policy-enforced channels (hard gate).
// For public policy-enforced channels, ABAC is advisory — only apply the
// filter when the caller explicitly asks via abac_match_only=true so callers
// like the invite modal can fetch all team members and annotate the matching
// subset without the list being narrowed for them.
//
// Surface ChannelAccessControlled errors instead of silently swallowing
// them — a transient store / license read failure here would otherwise
// fall through to the unfiltered path and could expose users a hard-gated
// private channel was configured to hide.
abacMatchOnly, _ := strconv.ParseBool(r.URL.Query().Get("abac_match_only"))
useAbacFilter := false
enforced, enforcedErr := c.App.ChannelAccessControlled(c.AppContext, notInChannelId)
if enforcedErr != nil {
c.Err = enforcedErr
return
}
if enforced {
ch, chErr := c.App.GetChannel(c.AppContext, notInChannelId)
if chErr != nil {
c.Err = chErr
return
}
useAbacFilter = ch.Type == model.ChannelTypePrivate || abacMatchOnly
}
if useAbacFilter {
cursorId := r.URL.Query().Get("cursor_id")
profiles, appErr = c.App.GetUsersNotInAbacChannel(c.AppContext, inTeamId, notInChannelId, groupConstrainedBool, cursorId, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else {
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
}
} else if notInTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), notInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
etag = c.App.GetUsersNotInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users Not in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersNotInTeamPage(notInTeamId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if inTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), inTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if sort == "last_activity_at" {
profiles, appErr = c.App.GetRecentlyActiveUsersForTeamPage(c.AppContext, inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if sort == "create_at" {
profiles, appErr = c.App.GetNewUsersForTeamPage(c.AppContext, inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else {
etag = c.App.GetUsersInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersInTeamPage(userGetOptions, c.IsSystemAdmin())
}
} else if inChannelId != "" {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), inChannelId, model.PermissionReadChannel); !ok {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if sort == "status" {
profiles, appErr = c.App.GetUsersInChannelPageByStatus(userGetOptions, c.IsSystemAdmin())
} else if sort == "admin" {
profiles, appErr = c.App.GetUsersInChannelPageByAdmin(userGetOptions, c.IsSystemAdmin())
} else {
profiles, appErr = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin())
}
} else if inGroupId != "" {
if gErr := hasPermissionToReadGroupMembers(c, inGroupId); gErr != nil {
gErr.Where = "Api.getUsers"
c.Err = gErr
return
}
if sort == "display_name" {
var user *model.User
user, appErr = c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
profiles, _, appErr = c.App.GetGroupMemberUsersSortedPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions, c.App.GetNotificationNameFormat(user))
} else {
profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
}
} else if notInGroupId != "" {
appErr = hasPermissionToReadGroupMembers(c, notInGroupId)
if appErr != nil {
appErr.Where = "Api.getUsers"
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
if appErr != nil {
c.Err = appErr
return
}
} else {
userGetOptions, appErr = c.App.RestrictUsersGetByPermissions(c.AppContext, c.AppContext.Session().UserId, userGetOptions)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin())
}
if appErr != nil {
c.Err = appErr
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("getUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
userIDs, err := model.SortedArrayFromJSON(r.Body)
if err != nil {
c.Err = model.NewAppError("getUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
return
} else if len(userIDs) == 0 {
c.SetInvalidParam("user_ids")
return
}
sinceString := r.URL.Query().Get("since")
options := &store.UserGetByIdsOpts{
IsAdmin: c.IsSystemAdmin(),
}
if sinceString != "" {
since, sErr := strconv.ParseInt(sinceString, 10, 64)
if sErr != nil {
c.SetInvalidParamWithErr("since", sErr)
return
}
options.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
options.ViewRestrictions = restrictions
users, appErr := c.App.GetUsersByIds(c.AppContext, userIDs, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
usernames, err := model.SortedArrayFromJSON(r.Body)
if err != nil {
c.Err = model.NewAppError("getUsersByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
return
} else if len(usernames) == 0 {
c.SetInvalidParam("usernames")
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
users, appErr := c.App.GetUsersByUsernames(usernames, c.IsSystemAdmin(), restrictions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getKnownUsers(c *Context, w http.ResponseWriter, r *http.Request) {
userIDs, appErr := c.App.GetKnownUsers(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(userIDs)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var props model.UserSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("props", err)
return
}
if props.Limit == 0 {
props.Limit = model.UserSearchDefaultLimit
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
if props.TeamId == "" && props.NotInChannelId != "" {
c.SetInvalidParam("team_id")
return
}
if props.InGroupId != "" {
if appErr := hasPermissionToReadGroupMembers(c, props.InGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.NotInGroupId != "" {
if appErr := hasPermissionToReadGroupMembers(c, props.NotInGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.InChannelId != "" {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.InChannelId, model.PermissionReadChannel); !ok {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
if props.NotInChannelId != "" {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.NotInChannelId, model.PermissionReadChannel); !ok {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
if props.TeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.NotInTeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.NotInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.Limit <= 0 || props.Limit > model.UserSearchMaxLimit {
c.SetInvalidParam("limit")
return
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
AllowInactive: props.AllowInactive,
GroupConstrained: props.GroupConstrained,
Limit: props.Limit,
Role: props.Role,
Roles: props.Roles,
ChannelRoles: props.ChannelRoles,
TeamRoles: props.TeamRoles,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowEmails = true
options.AllowFullNames = true
} else {
options.AllowEmails = *c.App.Config().PrivacySettings.ShowEmailAddress
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
options, appErr := c.App.RestrictUsersSearchByPermissions(c.AppContext, c.AppContext.Session().UserId, options)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr := c.App.SearchUsers(c.AppContext, &props, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("searchUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
channelId := r.URL.Query().Get("in_channel")
teamId := r.URL.Query().Get("in_team")
name := r.URL.Query().Get("name")
limitStr := r.URL.Query().Get("limit")
limit, _ := strconv.Atoi(limitStr)
if limitStr == "" {
limit = model.UserSearchDefaultLimit
} else if limit > model.UserSearchMaxLimit {
limit = model.UserSearchMaxLimit
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
// Never autocomplete on emails.
AllowEmails: false,
Limit: limit,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowFullNames = true
} else {
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
if channelId != "" {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel); !ok {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
if teamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
var autocomplete model.UserAutocomplete
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.AppContext, c.AppContext.Session().UserId, options)
if err != nil {
c.Err = err
return
}
if channelId != "" {
// We're using the channelId to search for users inside that channel and the team
// to get the not in channel list. Also we want to include the DM and GM users for
// that team which could only be obtained having the team id.
if teamId == "" {
c.Err = model.NewAppError("autocompleteUser",
"api.user.autocomplete_users.missing_team_id.app_error",
nil,
"channelId="+channelId,
http.StatusInternalServerError,
)
return
}
result, err := c.App.AutocompleteUsersInChannel(c.AppContext, teamId, channelId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InChannel
autocomplete.OutOfChannel = result.OutOfChannel
} else if teamId != "" {
result, err := c.App.AutocompleteUsersInTeam(c.AppContext, teamId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InTeam
} else {
result, err := c.App.SearchUsersInTeam(c.AppContext, "", name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result
}
// Fetch agent users for autocomplete
agentUsers, appErr := c.App.GetUsersForAgents(c.AppContext, c.AppContext.Session().UserId)
if appErr == nil && agentUsers != nil {
autocomplete.Agents = agentUsers
}
if err := json.NewEncoder(w).Encode(autocomplete); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "user", &user)
// The user being updated in the payload must be the same one as indicated in the URL.
if user.Id != c.Params.UserId {
c.SetInvalidParam("user_id")
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), user.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(user.Id)
if err != nil {
c.Err = err
return
}
// Cannot update a system admin unless user making request is a systemadmin also.
if ouser.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
if c.AppContext.Session().IsOAuth {
if ouser.Email != user.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
// Check that the fields being updated are not set by the login provider
conflictField := c.App.CheckProviderAttributes(c.AppContext, ouser, user.ToPatch())
if conflictField != "" {
c.Err = model.NewAppError(
"updateUser", "api.user.update_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if user.Email != "" && ouser.Email != user.Email && c.AppContext.Session().UserId == c.Params.UserId {
err = c.App.DoubleCheckPassword(c.AppContext, ouser, user.Password)
if err != nil {
c.SetInvalidParam("password")
return
}
}
ruser, err := c.App.UpdateUserAsUser(c.AppContext, &user, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var patch model.UserPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
patch.RemoteId = nil
auditRec := c.MakeAuditRecord(model.AuditEventPatchUser, model.AuditStatusFail)
model.AddEventParameterAuditableToAuditRec(auditRec, "user_patch", &patch)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidParam("user_id")
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if ouser.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.AppContext.Session().IsOAuth && patch.Email != nil {
if ouser.Email != *patch.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
conflictField := c.App.CheckProviderAttributes(c.AppContext, ouser, &patch)
if conflictField != "" {
c.Err = model.NewAppError(
"patchUser", "api.user.patch_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if patch.Email != nil && ouser.Email != *patch.Email && c.AppContext.Session().UserId == c.Params.UserId {
if patch.Password == nil {
c.SetInvalidParam("password")
return
}
if err = c.App.DoubleCheckPassword(c.AppContext, ouser, *patch.Password); err != nil {
c.Err = err
return
}
}
ruser, err := c.App.PatchUser(c.AppContext, c.Params.UserId, &patch, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
c.App.SetAutoResponderStatus(c.AppContext, ruser, ouser.NotifyProps)
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userId := c.Params.UserId
permanent := c.Params.Permanent
auditRec := c.MakeAuditRecord(model.AuditEventDeleteUser, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", userId)
model.AddEventParameterToAuditRec(auditRec, "permanent", permanent)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), userId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if c.Params.UserId == c.AppContext.Session().UserId && !*c.App.Config().TeamSettings.EnableUserDeactivation && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.Err = model.NewAppError("deleteUser", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(userId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if permanent {
if *c.App.Config().ServiceSettings.EnableAPIUserDeletion {
err = c.App.PermanentDeleteUser(c.AppContext, user)
} else {
loggedUser, usrErr := c.App.GetUser(c.AppContext.Session().UserId)
if usrErr == nil && loggedUser != nil && loggedUser.IsSystemAdmin() {
// More verbose error message for system admins
err = model.NewAppError("deleteUser", "api.user.delete_user.not_enabled.for_admin.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
} else {
err = model.NewAppError("deleteUser", "api.user.delete_user.not_enabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
}
}
} else {
_, err = c.App.UpdateActive(c.AppContext, user, false)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !model.IsValidUserRoles(newRoles) {
c.SetInvalidParam("roles")
return
}
// require license feature to assign "new system roles"
for roleName := range strings.FieldsSeq(newRoles) {
for _, id := range model.NewSystemRoleIDs {
if roleName == id {
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomPermissionsSchemes {
c.Err = model.NewAppError("updateUserRoles", "api.user.update_user_roles.license.app_error", nil, "", http.StatusBadRequest)
return
}
}
}
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateUserRoles, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "roles", newRoles)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageRoles) {
c.SetPermissionError(model.PermissionManageRoles)
return
}
user, err := c.App.UpdateUserRoles(c.AppContext, c.Params.UserId, newRoles, true)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
c.LogAudit(fmt.Sprintf("user=%s roles=%s", c.Params.UserId, newRoles))
ReturnStatusOK(w)
}
func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
active, ok := props["active"].(bool)
if !ok {
c.SetInvalidParam("active")
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateUserActive, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "active", active)
// true when you're trying to de-activate yourself
isSelfDeactivate := !active && c.Params.UserId == c.AppContext.Session().UserId
if !isSelfDeactivate && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers) {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.permissions.app_error", nil, "userId="+c.Params.UserId, http.StatusForbidden)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if isSelfDeactivate && !*c.App.Config().TeamSettings.EnableUserDeactivation {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if user.IsBot {
if permErr := c.App.SessionHasPermissionToManageBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId); permErr != nil {
c.Err = permErr
return
}
}
if active && user.IsGuest() && !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
if user.AuthService == model.UserAuthServiceLdap {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.cannot_modify_status_when_user_is_managed_by_ldap.app_error", nil, "userId="+c.Params.UserId, http.StatusForbidden)
return
}
if _, err = c.App.UpdateActive(c.AppContext, user, active); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit(fmt.Sprintf("user_id=%s active=%v", user.Id, active))
if isSelfDeactivate {
c.App.Srv().Go(func() {
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, "", http.StatusInternalServerError).Wrap(err))
}
})
}
message := model.NewWebSocketEvent(model.WebsocketEventUserActivationStatusChange, "", "", "", nil, "")
c.App.Publish(message)
ReturnStatusOK(w)
}
func updateUserAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateUserAuth, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
var userAuth model.UserAuth
if jsonErr := json.NewDecoder(r.Body).Decode(&userAuth); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "user_auth", &userAuth)
if userAuth.AuthData == nil || *userAuth.AuthData == "" || userAuth.AuthService == "" {
c.Err = model.NewAppError("updateUserAuth", "api.user.update_user_auth.invalid_request", nil, "", http.StatusBadRequest)
return
}
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
auditRec.AddEventPriorState(user)
}
user, err := c.App.UpdateUserAuth(c.AppContext, c.Params.UserId, &userAuth)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
auditRec.Success()
auditRec.AddMeta("auth_service", user.AuthService)
c.LogAudit(fmt.Sprintf("updated user %s auth to service=%v", c.Params.UserId, user.AuthService))
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateUserMfa, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if appErr := c.App.MFARequired(c.AppContext); !c.AppContext.Session().Local && c.AppContext.Session().UserId != c.Params.UserId && appErr != nil {
c.Err = appErr
return
}
if user, appErr := c.App.GetUser(c.Params.UserId); appErr == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
props := model.StringInterfaceFromJSON(r.Body)
activate, ok := props["activate"].(bool)
if !ok {
c.SetInvalidParam("activate")
return
}
code := ""
if activate {
code, ok = props["code"].(string)
if !ok || code == "" {
c.SetInvalidParam("code")
return
}
}
c.LogAudit("attempt")
if appErr := c.App.UpdateMfa(c.AppContext, activate, c.Params.UserId, code); appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddMeta("activate", activate)
c.LogAudit("success - mfa updated")
ReturnStatusOK(w)
}
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
secret, err := c.App.GenerateMfaSecret(c.Params.UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if err := json.NewEncoder(w).Encode(secret); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord(model.AuditEventUpdatePassword, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempted")
var canUpdatePassword bool
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
if user.IsSystemAdmin() {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
} else if user.IsBot {
canUpdatePassword = c.App.SessionHasPermissionToManageBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) == nil
} else {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers)
}
}
var err *model.AppError
// There are two main update flows depending on whether the provided password
// is already hashed or not.
if props["already_hashed"] == "true" {
if canUpdatePassword {
err = c.App.UpdateHashedPasswordByUserId(c.Params.UserId, newPassword)
} else if c.Params.UserId == c.AppContext.Session().UserId {
err = model.NewAppError("updatePassword", "api.user.update_password.user_and_hashed.app_error", nil, "", http.StatusUnauthorized)
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
} else {
if c.Params.UserId == c.AppContext.Session().UserId {
currentPassword := props["current_password"]
if currentPassword == "" {
c.SetInvalidParam("current_password")
return
}
err = c.App.UpdatePasswordAsUser(c.AppContext, c.Params.UserId, currentPassword, newPassword)
} else if canUpdatePassword {
err = c.App.UpdatePasswordByUserIdSendEmail(c.AppContext, c.Params.UserId, newPassword, c.AppContext.T("api.user.reset_password.method"))
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
}
if err != nil {
c.LogAudit("failed")
c.Err = err
return
}
auditRec.Success()
c.LogAudit("completed")
ReturnStatusOK(w)
}
func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord(model.AuditEventResetPassword, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
tokenPrefix := token[:5]
c.LogAudit("attempt - token_prefix=" + tokenPrefix)
if err := c.App.ResetPasswordFromToken(c.AppContext, token, newPassword); err != nil {
c.LogAudit("fail - token_prefix=" + tokenPrefix)
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_prefix=" + tokenPrefix)
ReturnStatusOK(w)
}
func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
auditRec := c.MakeAuditRecord(model.AuditEventSendPasswordReset, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "email", email)
sent, err := c.App.SendPasswordReset(c.AppContext, email, c.App.GetSiteURL())
if err != nil {
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
ReturnStatusOK(w)
} else {
c.Err = err
}
return
}
if sent {
auditRec.Success()
c.LogAudit("sent=" + email)
}
ReturnStatusOK(w)
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
// Mask all sensitive errors, with the exception of the following
defer func() {
if c.Err == nil {
return
}
unmaskedErrors := []string{
"mfa.validate_token.authenticate.app_error",
"api.user.check_user_mfa.bad_code.app_error",
"api.user.login.blank_pwd.app_error",
"api.user.login.bot_login_forbidden.app_error",
"api.user.login.remote_users.login.error",
"api.user.login.client_side_cert.certificate.app_error",
"api.user.login.inactive.app_error",
"api.user.login.not_verified.app_error",
"api.user.check_user_login_attempts.too_many.app_error",
"app.team.join_user_to_team.max_accounts.app_error",
"store.sql_user.save.max_accounts.app_error",
"api.user.check_user_login_attempts.too_many_ldap.app_error",
}
maskError := true
for _, unmaskedError := range unmaskedErrors {
if c.Err.Id == unmaskedError {
maskError = false
}
}
if !maskError {
return
}
config := c.App.Config()
enableUsername := *config.EmailSettings.EnableSignInWithUsername
enableEmail := *config.EmailSettings.EnableSignInWithEmail
samlEnabled := *config.SamlSettings.Enable
gitlabEnabled := *config.GitLabSettings.Enable
openidEnabled := *config.OpenIdSettings.Enable
googleEnabled := *config.GoogleSettings.Enable
office365Enabled := *config.Office365Settings.Enable
if samlEnabled || gitlabEnabled || googleEnabled || office365Enabled || openidEnabled {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_sso", nil, "", http.StatusUnauthorized)
return
}
if enableUsername && !enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_username", nil, "", http.StatusUnauthorized)
return
}
if !enableUsername && enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email", nil, "", http.StatusUnauthorized)
return
}
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email_username", nil, "", http.StatusUnauthorized)
}()
props := model.MapFromJSON(r.Body)
id := props["id"]
loginId := props["login_id"]
password := props["password"]
mfaToken := props["token"]
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
magicLinkToken := props["magic_link_token"]
auditRec := c.MakeAuditRecord(model.AuditEventLogin, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "device_id", deviceId)
var user *model.User
var err *model.AppError
if magicLinkToken != "" {
auditRec.AddMeta("login_method", "guest_magic_link")
c.LogAudit("attempt - guest_magic_link")
if !*c.App.Config().GuestAccountsSettings.EnableGuestMagicLink {
c.Err = model.NewAppError("login", "api.user.login.guest_magic_link.disabled.error", nil, "", http.StatusUnauthorized)
return
}
user, err = c.App.AuthenticateUserForGuestMagicLink(c.AppContext, magicLinkToken)
if err != nil {
c.LogAudit("failure - guest_magic_link")
c.Err = err
return
}
} else {
model.AddEventParameterToAuditRec(auditRec, "login_id", loginId)
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
user, err = c.App.AuthenticateUserForLogin(c.AppContext, id, loginId, password, mfaToken, "", ldapOnly)
if err != nil {
c.LogAuditWithUserId(id, "failure - login_id="+loginId)
c.Err = err
return
}
}
auditRec.AddEventResultState(user)
if user.IsMagicLinkEnabled() {
if !*c.App.Config().GuestAccountsSettings.EnableGuestMagicLink {
c.Err = model.NewAppError("login", "api.user.login.guest_magic_link.disabled.error", nil, "", http.StatusUnauthorized)
return
}
}
if user.IsGuest() {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.license.error", nil, "", http.StatusUnauthorized)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.disabled.error", nil, "", http.StatusUnauthorized)
return
}
}
if user.IsRemote() {
c.Err = model.NewAppError("login", "api.user.login.remote_users.login.error", nil, "", http.StatusUnauthorized)
return
}
c.LogAuditWithUserId(user.Id, "authenticated")
isMobileDevice := utils.IsMobileRequest(r)
session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, isMobileDevice, false, false)
if err != nil {
c.Err = err
return
}
c.AppContext = c.AppContext.WithSession(session)
c.LogAuditWithUserId(user.Id, "success")
if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
c.App.AttachSessionCookies(c.AppContext, w, r)
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
user.Sanitize(map[string]bool{})
auditRec.Success()
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func loginWithDesktopToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
deviceId := props["device_id"]
auditRec := c.MakeAuditRecord(model.AuditEventLoginWithDesktopToken, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("login_method", "desktop_token")
model.AddEventParameterToAuditRec(auditRec, "device_id", deviceId)
user, err := c.App.ValidateDesktopToken(token, time.Now().Add(-model.DesktopTokenTTL).Unix())
if err != nil {
c.Err = err
return
}
isOAuthUser := user.IsOAuthUser()
isSamlUser := user.IsSAMLUser()
if !isOAuthUser && !isSamlUser {
c.Err = model.NewAppError("loginWithDesktopToken", "api.user.login_with_desktop_token.not_oauth_or_saml_user.app_error", nil, "", http.StatusUnauthorized)
return
}
session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, isOAuthUser, isSamlUser)
if err != nil {
c.Err = err
return
}
c.AppContext = c.AppContext.WithSession(session)
c.App.AttachSessionCookies(c.AppContext, w, r)
auditRec.Success()
c.LogAuditWithUserId(user.Id, "success")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
campaignToURL := map[string]string{
"focalboard": "/boards",
}
useCaseToURL := map[string]string{
"mission-ops": "/mission-ops-hq",
"dev-sec-ops": "/dev-sec-ops-hq",
"cyber-defense": "/cyber-defense-hq",
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
c.Logger.Warn("Failed to parse form data", mlog.Err(err))
}
var loginID string
var token string
var campaign string
var useCase string
if len(r.Form) > 0 {
for key, value := range r.Form {
if key == "login_id" {
loginID = value[0]
}
if key == "cws_token" {
token = value[0]
}
if key == "utm_campaign" {
campaign = value[0]
}
if key == "use_case" {
useCase = value[0]
}
}
}
auditRec := c.MakeAuditRecord(model.AuditEventLogin, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "login_id", loginID)
user, err := c.App.AuthenticateUserForLogin(c.AppContext, "", loginID, "", "", token, false)
if err != nil {
c.LogAuditWithUserId("", "failure - login_id="+loginID)
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
c.LogAuditWithUserId(user.Id, "authenticated")
isMobileDevice := utils.IsMobileRequest(r)
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobileDevice, false, false)
if err != nil {
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
c.AppContext = c.AppContext.WithSession(session)
c.LogAuditWithUserId(user.Id, "success")
c.App.AttachSessionCookies(c.AppContext, w, r)
redirectURL := *c.App.Config().ServiceSettings.SiteURL
if campaign != "" {
if url, ok := campaignToURL[campaign]; ok {
redirectURL += url
}
}
// If a cloud preview, redirect to the correct use case URL
if c.App.License().IsCloudPreview() && useCase != "" {
if url, ok := useCaseToURL[useCase]; ok {
redirectURL += url
}
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func getLoginType(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
id := props["id"]
loginId := props["login_id"]
deviceId := props["device_id"]
// For the time being, we only support getting the login type when
// guest magic link is enabled. We can consider adding support for other
// login methods in the future, and this check may be removed.
if !*c.App.Config().GuestAccountsSettings.EnableGuestMagicLink ||
!*c.App.Config().GuestAccountsSettings.Enable ||
!*c.App.Channels().License().Features.GuestAccounts {
w.WriteHeader(http.StatusNotFound)
return
}
if loginId == "" {
c.SetInvalidParam("login_id")
return
}
auditRec := c.MakeAuditRecord(model.AuditEventLogin, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "login_id", loginId)
model.AddEventParameterToAuditRec(auditRec, "device_id", deviceId)
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
user, err := c.App.GetUserForLogin(c.AppContext, id, loginId)
if err != nil {
c.Logger.Debug("Could not get user for login", mlog.Err(err))
if err := json.NewEncoder(w).Encode(model.LoginTypeResponse{
AuthService: "",
}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if user.DeleteAt > 0 {
if err := json.NewEncoder(w).Encode(model.LoginTypeResponse{
AuthService: "",
IsDeactivated: true,
}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
c.LogAuditWithUserId(user.Id, "found user for login_id="+loginId)
auditRec.AddEventResultState(user)
auditRec.Success()
canSendMagicLinkEmail := func(user *model.User) bool {
if !user.IsGuest() {
return false
}
if !user.IsMagicLinkEnabled() {
return false
}
if c.App.Channels().License() == nil {
return false
}
if !*c.App.Channels().License().Features.GuestAccounts {
return false
}
if !*c.App.Config().GuestAccountsSettings.EnableGuestMagicLink {
return false
}
return true
}
if canSendMagicLinkEmail(user) {
eErr := c.App.Srv().EmailService.SendMagicLinkEmailSelfService(c.AppContext, user.Email, c.App.GetSiteURL())
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
c.Err = model.NewAppError("getLoginType", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s", user.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
c.Err = model.NewAppError("getLoginType", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, error=%v", user.Id, eErr), http.StatusInternalServerError)
default:
c.Err = model.NewAppError("getLoginType", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, error=%v", user.Id, eErr), http.StatusRequestEntityTooLarge)
}
return
}
c.Logger.Debug("Guest magic link email sent successfully", mlog.String("user_id", user.Id))
if jErr := json.NewEncoder(w).Encode(model.LoginTypeResponse{
AuthService: model.UserAuthServiceMagicLink,
}); jErr != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if err := json.NewEncoder(w).Encode(model.LoginTypeResponse{
AuthService: "",
}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func logout(c *Context, w http.ResponseWriter, r *http.Request) {
Logout(c, w, r)
}
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord(model.AuditEventLogout, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
// Determine detailed authentication status for audit record (MM-67140)
var authStatus string
if c.AppContext.Session().UserId != "" {
authStatus = "authenticated"
} else {
_, tokenLocation := app.ParseAuthTokenFromRequest(r)
if tokenLocation == app.TokenLocationNotFound {
authStatus = "no_token"
} else {
authStatus = "token_invalid"
}
}
model.AddEventParameterToAuditRec(auditRec, "auth_status", authStatus)
c.LogAudit("")
c.RemoveSessionCookie(w, r)
if c.AppContext.Session().Id != "" {
if err := c.App.RevokeSessionById(c.AppContext, c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
}
auditRec.Success()
ReturnStatusOK(w)
}
func getSessions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
sessions, appErr := c.App.GetSessions(c.AppContext, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
for _, session := range sessions {
session.Sanitize()
}
js, err := json.Marshal(sessions)
if err != nil {
c.Err = model.NewAppError("getSessions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventRevokeSession, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
props := model.MapFromJSON(r.Body)
sessionId := props["session_id"]
if sessionId == "" {
c.SetInvalidParam("session_id")
return
}
model.AddEventParameterToAuditRec(auditRec, "session_id", sessionId)
session, err := c.App.GetSessionById(c.AppContext, sessionId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(session)
auditRec.AddEventObjectType("session")
if session.UserId != c.Params.UserId {
c.SetInvalidURLParam("user_id")
return
}
if err := c.App.RevokeSession(c.AppContext, session); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventRevokeAllSessionsForUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RevokeAllSessions(c.AppContext, c.Params.UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsAllUsers(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventRevokeAllSessionsAllUsers, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if err := c.App.RevokeSessionsFromAllUsers(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func handleDeviceProps(c *Context, w http.ResponseWriter, r *http.Request) {
receivedProps := model.MapFromJSON(r.Body)
deviceId := receivedProps["device_id"]
newProps := map[string]string{}
deviceNotificationsDisabled := receivedProps[model.SessionPropDeviceNotificationDisabled]
if deviceNotificationsDisabled != "" {
if deviceNotificationsDisabled != "false" && deviceNotificationsDisabled != "true" {
c.SetInvalidParam(model.SessionPropDeviceNotificationDisabled)
return
}
newProps[model.SessionPropDeviceNotificationDisabled] = deviceNotificationsDisabled
}
mobileVersion := receivedProps[model.SessionPropMobileVersion]
if mobileVersion != "" {
if _, err := semver.StrictNewVersion(mobileVersion); err != nil {
c.SetInvalidParam(model.SessionPropMobileVersion)
return
}
newProps[model.SessionPropMobileVersion] = mobileVersion
}
if deviceId != "" {
attachDeviceId(c, w, r, deviceId)
}
if c.Err != nil {
return
}
if err := c.App.SetExtraSessionProps(c.AppContext.Session(), newProps); err != nil {
c.Err = err
return
}
c.App.ClearSessionCacheForUser(c.AppContext.Session().UserId)
ReturnStatusOK(w)
}
func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request, deviceId string) {
auditRec := c.MakeAuditRecord(model.AuditEventAttachDeviceId, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "device_id", deviceId)
// A special case where we logout of all other sessions with the same device id
if err := c.App.RevokeSessionsForDeviceId(c.AppContext, c.AppContext.Session().UserId, deviceId, c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
c.App.ClearSessionCacheForUser(c.AppContext.Session().UserId)
c.App.SetSessionExpireInHours(c.AppContext.Session(), *c.App.Config().ServiceSettings.SessionLengthMobileInHours)
maxAgeSeconds := *c.App.Config().ServiceSettings.SessionLengthMobileInHours * 60 * 60
secure := false
if app.GetProtocol(r) == "https" {
secure = true
}
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: c.AppContext.Session().Token,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Domain: c.App.GetCookieDomain(),
Secure: secure,
}
if secure && utils.CheckEmbeddedCookie(r) {
sessionCookie.SameSite = http.SameSiteNoneMode
}
http.SetCookie(w, sessionCookie)
if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
}
func getUserAudits(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventGetUserAudits, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
audits, err := c.App.GetAuditsPage(c.AppContext, c.Params.UserId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("page", c.Params.Page)
auditRec.AddMeta("audits_per_page", c.Params.LogsPerPage)
if err := json.NewEncoder(w).Encode(audits); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func verifyUserEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
auditRec := c.MakeAuditRecord(model.AuditEventVerifyUserEmail, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if err := c.App.VerifyEmailFromToken(c.AppContext, token); err != nil {
c.Err = model.NewAppError("verifyUserEmail", "api.user.verify_email.bad_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
auditRec.Success()
c.LogAudit("Email Verified")
ReturnStatusOK(w)
}
func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord(model.AuditEventSendVerificationEmail, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "email", email)
model.AddEventParameterToAuditRec(auditRec, "redirect", redirect)
user, err := c.App.GetUserForLogin(c.AppContext, "", email)
if err != nil {
// Don't want to leak whether the email is valid or not
ReturnStatusOK(w)
return
}
auditRec.AddEventResultState(user)
if err = c.App.SendEmailVerification(user, user.Email, redirect); err != nil {
// Don't want to leak whether the email is valid or not
c.LogErrorByCode(err)
ReturnStatusOK(w)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) {
var switchRequest model.SwitchRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&switchRequest); jsonErr != nil {
c.SetInvalidParamWithErr("switch_request", jsonErr)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventSwitchAccountType, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "switch_request", &switchRequest)
link := ""
var err *model.AppError
if switchRequest.EmailToOAuth() {
link, err = c.App.SwitchEmailToOAuth(c.AppContext, w, r, switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.NewService)
} else if switchRequest.OAuthToEmail() {
c.SessionRequired()
if c.Err != nil {
return
}
link, err = c.App.SwitchOAuthToEmail(c.AppContext, switchRequest.Email, switchRequest.NewPassword, c.AppContext.Session().UserId)
} else if switchRequest.EmailToLdap() {
link, err = c.App.SwitchEmailToLdap(c.AppContext, switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapLoginId, switchRequest.NewPassword)
} else if switchRequest.LdapToEmail() {
link, err = c.App.SwitchLdapToEmail(c.AppContext, switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword)
} else {
c.SetInvalidParam("switch_request")
return
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
if _, err := w.Write([]byte(model.MapToJSON(map[string]string{"follow_link": link}))); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventCreateUserAccessToken, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
if user.IsRemote() {
// remote/synthetic users cannot have access tokens
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
var accessToken model.UserAccessToken
if jsonErr := json.NewDecoder(r.Body).Decode(&accessToken); jsonErr != nil {
c.SetInvalidParamWithErr("user_access_token", jsonErr)
return
}
if accessToken.Description == "" {
c.SetInvalidParam("description")
return
}
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
accessToken.UserId = c.Params.UserId
accessToken.Token = ""
token, err := c.App.CreateUserAccessToken(c.AppContext, &accessToken)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("token_id", token.Id)
c.LogAudit("success - token_id=" + token.Id)
if err := json.NewEncoder(w).Encode(token); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
var props model.UserAccessTokenSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("user_access_token_search", err)
return
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
accessTokens, appErr := c.App.SearchUserAccessTokens(props.Term)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
accessTokens, appErr := c.App.GetUserAccessTokens(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserAccessTokensForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
accessTokens, appErr := c.App.GetUserAccessTokensForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTokenId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
accessToken, appErr := c.App.GetUserAccessToken(c.Params.TokenId, true)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord(model.AuditEventRevokeUserAccessToken, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "token_id", tokenId)
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.RevokeUserAccessToken(c.AppContext, accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func disableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord(model.AuditEventDisableUserAccessToken, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "token_id", tokenId)
defer c.LogAuditRec(auditRec)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.DisableUserAccessToken(c.AppContext, accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord(model.AuditEventEnableUserAccessToken, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "token_id", tokenId)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(c.AppContext, *c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.EnableUserAccessToken(c.AppContext, accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func saveUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
auditRec := c.MakeAuditRecord(model.AuditEventSaveUserTermsOfService, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
userId := c.AppContext.Session().UserId
termsOfServiceId, ok := props["termsOfServiceId"].(string)
if !ok {
c.SetInvalidParam("termsOfServiceId")
return
}
model.AddEventParameterToAuditRec(auditRec, "terms_of_service_id", termsOfServiceId)
accepted, ok := props["accepted"].(bool)
if !ok {
c.SetInvalidParam("accepted")
return
}
model.AddEventParameterToAuditRec(auditRec, "accepted", accepted)
if user, err := c.App.GetUser(userId); err == nil {
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
}
if _, err := c.App.GetTermsOfService(termsOfServiceId); err != nil {
c.Err = err
return
}
if err := c.App.SaveUserTermsOfService(userId, termsOfServiceId, accepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("TermsOfServiceId=" + termsOfServiceId + ", accepted=" + strconv.FormatBool(accepted))
ReturnStatusOK(w)
}
func getUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
result, err := c.App.GetUserTermsOfService(userId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func promoteGuestToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventPromoteGuestToUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPromoteGuest) {
c.SetPermissionError(model.PermissionPromoteGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
if !user.IsGuest() {
c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.user.promote_guest_to_user.no_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if user.IsMagicLinkEnabled() {
c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.user.promote_guest_to_user.magic_link_enabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.PromoteGuestToUser(c.AppContext, user, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.license.error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.disabled.error", nil, "", http.StatusNotImplemented)
return
}
guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts
if !guestEnabled {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventDemoteUserToGuest, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDemoteToGuest) {
c.SetPermissionError(model.PermissionDemoteToGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.AddEventResultState(user)
if user.IsGuest() {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.user.demote_user_to_guest.already_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.DemoteUserToGuest(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func publishUserTyping(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var typingRequest model.TypingRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&typingRequest); jsonErr != nil {
c.SetInvalidParamWithErr("typing_request", jsonErr)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if ok, _ := c.App.HasPermissionToChannel(c.AppContext, c.Params.UserId, typingRequest.ChannelId, model.PermissionCreatePost); !ok {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if err := c.App.PublishUserTyping(c.Params.UserId, typingRequest.ChannelId, typingRequest.ParentId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func verifyUserEmailWithoutToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord(model.AuditEventVerifyUserEmailWithoutToken, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("user_id", user.Id)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.VerifyUserEmail(user.Id, user.Email); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("user verified")
c.App.SanitizeProfile(user, true)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertUserToBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord(model.AuditEventConvertUserToBot, model.AuditStatusFail)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "user", user)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
bot, appErr := c.App.ConvertUserToBot(c.AppContext, user)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
js, err := json.Marshal(bot)
if err != nil {
c.Err = model.NewAppError("convertUserToBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUploadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.Params.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("getUploadsForUser", "api.user.get_uploads_for_user.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
uss, appErr := c.App.GetUploadSessionsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(uss)
if err != nil {
c.Err = model.NewAppError("getUploadsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if _, err := w.Write(js); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
// For backward compatibility purposes
if c.Params.Page != -1 {
cursor := &model.ChannelMemberCursor{
Page: c.Params.Page,
PerPage: c.Params.PerPage,
}
members, err := c.App.GetChannelMembersWithTeamDataForUserWithPagination(c.AppContext, c.Params.UserId, cursor)
if err != nil {
c.Err = err
return
}
// Sanitize members for current user
currentUserId := c.AppContext.Session().UserId
for i := range members {
members[i].SanitizeForCurrentUser(currentUserId)
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
// The new model streams data using NDJSON format (each JSON object on a new line)
pageSize := 100
fromChannelID := ""
// Set the correct content type for NDJSON
w.Header().Set("Content-Type", "application/x-ndjson")
enc := json.NewEncoder(w)
for {
cursor := &model.ChannelMemberCursor{
Page: -1,
PerPage: pageSize,
FromChannelID: fromChannelID,
}
members, err := c.App.GetChannelMembersWithTeamDataForUserWithPagination(c.AppContext, c.Params.UserId, cursor)
if err != nil {
// If the page size was a perfect multiple of the total number of results,
// then the last query will always return zero results.
if fromChannelID != "" && err.Id == app.MissingChannelMemberError {
break
}
c.Err = err
return
}
currentUserId := c.AppContext.Session().UserId
for _, member := range members {
// Sanitize each member before encoding in the stream
member.SanitizeForCurrentUser(currentUserId)
if err := enc.Encode(member); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
if len(members) < pageSize {
break
}
fromChannelID = members[len(members)-1].ChannelId
}
}
func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "saml" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
matchField, ok := props["match_field"].(string)
if !ok {
c.SetInvalidParam("match_field")
return
}
auditRec := c.MakeAuditRecord(model.AuditEventMigrateAuthToLdap, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "from", from)
model.AddEventParameterToAuditRec(auditRec, "force", force)
model.AddEventParameterToAuditRec(auditRec, "match_field", matchField)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("api.migrateAuthToLDAP", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToLdap(c.AppContext, from, matchField, force, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.migrate_to_saml.error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "ldap" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
auto, ok := props["auto"].(bool)
if !ok {
c.SetInvalidParam("auto")
return
}
matches, ok := props["matches"].(map[string]any)
if !ok {
c.SetInvalidParam("matches")
return
}
usersMap := model.MapFromJSON(strings.NewReader(model.StringInterfaceToJSON(matches)))
auditRec := c.MakeAuditRecord(model.AuditEventMigrateAuthToSaml, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "from", from)
model.AddEventParameterToAuditRec(auditRec, "auto", auto)
model.AddEventParameterToAuditRec(auditRec, "users_map", usersMap)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.SAML {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToSaml(c.AppContext, from, usersMap, auto, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.migrate_to_saml.error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireThreadId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ok, isMember := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.ThreadId)
if !ok {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
extendedStr := r.URL.Query().Get("extended")
extended, _ := strconv.ParseBool(extendedStr)
threadMembership, err := c.App.GetThreadMembershipForUser(c.Params.UserId, c.Params.ThreadId)
if err != nil {
c.Err = err
return
}
thread, err := c.App.GetThreadForUser(c.AppContext, threadMembership, extended)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec := c.MakeAuditRecord(model.AuditEventGetThreadForUser, model.AuditStatusSuccess)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "thread_id", c.Params.ThreadId)
if !isMember {
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
}
}
func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
options := model.GetUserThreadsOpts{
Since: 0,
Before: "",
After: "",
PageSize: uint64(c.Params.PerPage),
Unread: false,
Extended: false,
Deleted: false,
TotalsOnly: false,
ThreadsOnly: false,
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, parseError := strconv.ParseUint(sinceString, 10, 64)
if parseError != nil {
c.SetInvalidParam("since")
return
}
options.Since = since
}
options.Before = r.URL.Query().Get("before")
options.After = r.URL.Query().Get("after")
totalsOnlyStr := r.URL.Query().Get("totalsOnly")
threadsOnlyStr := r.URL.Query().Get("threadsOnly")
excludeDirectStr := r.URL.Query().Get("excludeDirect")
options.TotalsOnly, _ = strconv.ParseBool(totalsOnlyStr)
options.ThreadsOnly, _ = strconv.ParseBool(threadsOnlyStr)
options.ExcludeDirect, _ = strconv.ParseBool(excludeDirectStr)
// parameters are mutually exclusive
if options.Before != "" && options.After != "" {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_params", nil, "", http.StatusBadRequest)
return
}
// parameters are mutually exclusive
if options.TotalsOnly && options.ThreadsOnly {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_only_params", nil, "", http.StatusBadRequest)
return
}
deletedStr := r.URL.Query().Get("deleted")
unreadStr := r.URL.Query().Get("unread")
extendedStr := r.URL.Query().Get("extended")
options.Deleted, _ = strconv.ParseBool(deletedStr)
options.Unread, _ = strconv.ParseBool(unreadStr)
options.Extended, _ = strconv.ParseBool(extendedStr)
threads, err := c.App.GetThreadsForUser(c.AppContext, c.Params.UserId, c.Params.TeamId, options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(threads); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTimestamp().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateReadStateThreadByUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
model.AddEventParameterToAuditRec(auditRec, "thread_id", c.Params.ThreadId)
model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId)
model.AddEventParameterToAuditRec(auditRec, "timestamp", c.Params.Timestamp)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ok, isMember := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.ThreadId)
if !ok {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
if !isMember {
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
}
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func setUnreadThreadByPostId(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequirePostId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventSetUnreadThreadByPostId, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
model.AddEventParameterToAuditRec(auditRec, "thread_id", c.Params.ThreadId)
model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId)
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ok, isMember := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.ThreadId)
if !ok {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
if !isMember {
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
}
// We want to make sure the thread is followed when marking as unread
// https://mattermost.atlassian.net/browse/MM-36430
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, true)
if err != nil {
c.Err = err
return
}
thread, err := c.App.UpdateThreadReadForUserByPost(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.PostId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUnfollowThreadByUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
model.AddEventParameterToAuditRec(auditRec, "thread_id", c.Params.ThreadId)
model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.ThreadId); !ok {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func followThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventFollowThreadByUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
model.AddEventParameterToAuditRec(auditRec, "thread_id", c.Params.ThreadId)
model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.ThreadId); !ok {
c.SetPermissionError(model.PermissionReadChannelContent)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, true)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateReadStateAllThreadsByUser, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.Params.UserId)
model.AddEventParameterToAuditRec(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Request) {
if *c.App.Config().TeamSettings.EnableOpenServer {
c.Err = model.NewAppError("GetUsersWithInvalidEmails", model.NoTranslation, nil, "TeamSettings.EnableOpenServer is enabled", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
users, appErr := c.App.GetUsersWithInvalidEmails(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(users)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func resetPasswordFailedAttempts(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
errParams := map[string]any{"userID": c.Params.UserId}
auditRec := c.MakeAuditRecord(model.AuditEventResetPasswordFailedAttempts, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers) {
c.Err = model.NewAppError("resetPasswordFailedAttempts", "api.user.reset_password_failed_attempts.permissions.app_error", errParams, "", http.StatusForbidden)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if user.AuthService != model.UserAuthServiceLdap && user.AuthService != "" {
c.Err = model.NewAppError("resetPasswordFailedAttempts", "api.user.reset_password_failed_attempts.ldap_and_email_only.app_error", errParams, "", http.StatusBadRequest)
return
}
if err := c.App.ResetPasswordFailedAttempts(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}