mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
* 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>
4027 lines
123 KiB
Go
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)
|
|
}
|