mattermost/server/channels/api4/user.go
Ben Schumacher 209606f15b
MM-68419: Add expires_at to PAT data model and enforce expiry at token validation (#36243)
* Add expires_at to PAT data model and enforce expiry at token validation

Adds an ExpiresAt field (int64 millis, 0 = never expires) to the
UserAccessToken model and DB table, enforces expiry when a PAT is used
to create a session, clamps the resulting session's ExpiresAt to the
token's expiry so cached sessions also honor it, ships a background job
(cleanup_expired_access_tokens) that periodically deletes expired
tokens along with any sessions minted from them, and emits audit events
for rejected and reaped expired tokens.

Refs: MM-68419

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Fix govet shadow warnings in DeleteExpired

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Stabilize expired PAT test by persisting an already-expired token

The previous variant created a live token, used it to mint a session,
then backdated the row and revoked the cached session to force a
re-validation. That flow was race-prone under parallel test execution
and was flagged as flaky in CI. Replace it with a direct store write
that persists the PAT with ExpiresAt already in the past, so
createSessionForUserAccessToken is exercised deterministically on the
first HTTP call and no session cache races are possible.

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Address PR review: batched cleanup, consistent filters, audit ordering

- server.go: initialize s.Audit before initJobs() so the cleanup worker
  never captures a nil audit logger.

- Replace DeleteExpired(cutoff) with DeleteByIds([]string) on
  UserAccessTokenStore. The worker now fetches a batch via
  GetExpiredBefore, emits one audit record per token, then deletes
  exactly that batch by id — guaranteeing 1:1 audit/delete pairing
  and eliminating the IsActive-filter mismatch between reads and
  deletes. The worker loops up to maxBatches (=1000) x batchLimit
  (=1000) rows per run and stops when GetExpiredBefore returns less
  than batchLimit or zero rows.

- GetExpiredBefore now selects an explicit column set that omits the
  secret Token column, so the PAT secret never travels from DB to app.

- DeleteByIds surfaces an error from RowsAffected instead of silently
  returning 0.

- Remove dead job.Data initialization in the worker.

- api4 test: set IsActive: true explicitly, walk the AppError chain
  and assert the specific Id app.user_access_token.expired so future
  401 regressions are caught.

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Use named returns in DeleteByIds and clean up expired fixture in test

- DeleteByIds now declares (deleted int64, err error) and uses bare
  returns on every error path so finalizeTransactionX can append a
  rollback failure to the returned error via merror.Append. Previously
  early returns short-circuited the deferred rollback's error
  contribution.
- Add the expired token to the test cleanup so all three fixtures are
  removed even on early test exit.

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Guard GetExpiredBefore against non-positive limit + tighten store test

- GetExpiredBefore now short-circuits when limit <= 0 and returns an
  empty slice without hitting the DB. This prevents the int -> uint64
  cast on a negative value from wrapping into an effectively unbounded
  query.
- Store test now asserts row.Token is empty for every row returned by
  GetExpiredBefore (not just the matched one) to catch any future
  query change that accidentally re-introduces the secret column.
- Added store-level coverage for the limit=0 and limit<0 short-circuit
  contract.

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Add session-clamping and worker tests; bump migration to 172

Address PR test-coverage analysis (issuecomment-4336010565):

- api4/user_test.go: add two subtests covering session.ExpiresAt
  behavior — clamped to token.ExpiresAt when the PAT has a non-zero
  ExpiresAt, and untouched (long-lived) when the PAT has no expiry.
- cleanup_expired_access_tokens/worker.go: extract the batching/audit/
  error orchestration into a package-private cleanupExpired() taking
  small interfaces (expiredTokenStore, auditRecorder) so it can be
  unit-tested without spinning up a job server.
- cleanup_expired_access_tokens/worker_test.go (new): seven unit tests
  cover happy path, empty result, full-batch -> next iteration,
  maxIter cap, GetExpiredBefore error propagation, DeleteByIds error
  propagation, and nil auditLogger guard.
- Bump migration 000170_add_expiresat_to_user_access_tokens to 000172
  to slot in behind the master-side 000170 (property_groups_version)
  and 000171 (drop_property_fields_protected_index).

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Fix PAT expiry audit ordering and cleanup scheduler gate

- Move IsExpired() check after EnableUserAccessTokens gate in
  createSessionForUserAccessToken so the AuditEventRejectExpiredUserAccessToken
  event only fires when PATs are active for the user, not when the feature
  is globally disabled.
- Tie the cleanup_expired_access_tokens scheduler to EnableUserAccessTokens
  so the hourly job does not schedule on servers where PATs are disabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove per-token audit logging from expired PAT cleanup job

Background system jobs do not emit audit events in this codebase —
only user/admin-initiated actions do. The cleanup worker's per-token
AuditEventExpireUserAccessToken records were inconsistent with that
pattern (cleanup_desktop_tokens and other session jobs log nothing).

Also removes the early s.Audit init in NewServer that existed solely
to supply a non-nil logger to the worker.

The AuditEventRejectExpiredUserAccessToken event (emitted by
createSessionForUserAccessToken when a live request is rejected) is
unchanged — that is an auth gate firing in response to a request and
warrants auditing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Replace hand-rolled IN-clause helpers with squirrel query builder

Remove placeholders() and idsToArgs() from DeleteByIds — squirrel's
sq.Eq{"column": slice} generates the IN clause and argument list
automatically, matching the pattern used throughout the sqlstore package.

Also restructures the sessions delete from a PostgreSQL-specific
USING join to a portable subquery, keeping both statements expressible
via the query builder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Drop redundant logger.Error calls in cleanup worker

SimpleWorker already logs any error returned from execute at the
Error level (base_workers.go:86). The extra logger.Error calls before
return were double-logging every failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use mlog.CreateConsoleTestLogger in cleanup worker tests

Replaces the hand-rolled newTestLogger helper with the established
mlog.CreateConsoleTestLogger(t) pattern used by other job tests in
this package (jobs_test.go, recap/worker_test.go). It wires cleanup
and test-runner output automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Consolidate cleanup worker tests into subtests

Groups the six top-level TestCleanupExpiredXxx functions under a single
TestCleanupExpired parent with t.Run subtests. One shared logger is
created at the parent level; each subtest gets its own fakeStore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert incidental configureAudit restructure in server.go

The separation of s.Audit init from configureAudit was an unintended
side effect of an earlier commit. Restore the original pattern where
configureAudit is only called when s.Audit was nil at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove api4 PAT expiry tests until API endpoint exists

The three tests (expired token rejected, session clamped, no-expiry
default) bypass the API to inject ExpiresAt via the store directly,
since no API endpoint exists yet to create tokens with an expiry.
They belong in the PR that adds that endpoint.

The same behaviors are covered at the appropriate layer by
storetest/user_access_token_store.go and model/user_access_token_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Prevent ExtendSessionExpiryIfNeeded from overriding PAT session expiry

PAT-authenticated sessions have their ExpiresAt clamped to the token's
ExpiresAt in createSessionForUserAccessToken. However, ExtendSessionExpiryIfNeeded
was resetting that expiry to now+SessionLengthWebInHours on the first
subsequent request, effectively bypassing PAT expiry for cached sessions.

Guard the extension to skip SessionTypeUserAccessToken sessions until
GetSessionLengthInMillis learns to return a length bounded by token.ExpiresAt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Strip ExpiresAt in create-token handler until API officially supports it

The JSON decoder populates the full accessToken struct from the request body,
and only UserId and Token were being overwritten before the store call. This
allowed clients to set an arbitrary expires_at (including 0 for non-expiring)
through the existing endpoint, contradicting the PR description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Clear session cache for affected users after DeleteByIds in cleanup worker

DeleteByIds removes sessions from the DB but did not invalidate the in-memory
session cache. This left stale sessions readable from cache until eviction,
inconsistent with the RevokeSession path.

Thread a clearSessionCache callback through MakeWorker and cleanupExpired.
After each successful batch delete, call it for each unique UserId in the
batch. The callback is deduplicated per batch to avoid redundant cache
invalidations when a user has multiple expired tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add partial index on useraccesstokens.expiresat

The cleanup job queries expiresat on every scheduled run (hourly). Without an
index this is a full sequential scan. Add a partial index WHERE expiresat > 0
to match the query's filter, keeping the index small since most tokens have no
expiry set. Mirrors the idx_sessions_expires_at pattern on the sessions table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Register JobTypeCleanupExpiredAccessTokens in job permission switches

Without entries in SessionHasPermissionToReadJob, SessionHasPermissionToCreateJob,
and SessionHasPermissionToManageJob, the job type falls through to (false, nil),
which API handlers treat as HTTP 400. This made the cleanup job invisible to
System Console and unmanageable via API (list, cancel, manual trigger all 400).

Add the job type to the PermissionManageJobs / PermissionReadJobs groups in
all three switches, matching how other internal jobs like JobTypeMigrations
are handled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Teach GetSessionLengthInMillis to honor PAT ExpiresAt

Replace the blunt guard in ExtendSessionExpiryIfNeeded with proper logic in
GetSessionLengthInMillis: for PAT sessions with a fixed ExpiresAt, return the
remaining lifetime instead of the configured web-session hours.

This means newExpiry = now + (ExpiresAt - now) = ExpiresAt, so extension never
pushes the session past the token's own expiry. The elapsed threshold
collapses to zero for such sessions, so no spurious DB writes occur either.

Non-expiring PAT sessions (ExpiresAt == 0) continue to use normal web-session
extension, which is correct behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Split expiresat index into separate non-transactional migration (000175)

CREATE INDEX CONCURRENTLY cannot run inside a transaction block. The morph
migration runner wraps each file in a transaction by default, causing the
combined migration to fail.

Split the index creation out of 000174 into a new 000175 migration file
with the -- morph:nontransactional directive, following the same pattern
used by 000135, 000155, 000173 and others. The 174 down migration no longer
needs to drop the index since 175 owns it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update Beginx call to renamed Begin (sqlx wrapper API change on master)

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:30:30 +02:00

4027 lines
123 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 = ""
// TODO: remove once the API officially supports setting expires_at; until
// then, strip any client-supplied value so that JSON-decoded requests cannot
// set an arbitrary (or zero) expiry through the create-token endpoint.
accessToken.ExpiresAt = 0
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)
}