From e07a535e69528ea9a4f4853d51cf930a79877ce7 Mon Sep 17 00:00:00 2001 From: Rick Batka Date: Thu, 19 Oct 2017 15:05:59 -0400 Subject: [PATCH] [PLT-7396] Add the ability to revoke user sessions in System Console > Users #7493 (#267) * revoe all sessions for a user, redux action and request * add tests, add logic to reducer so it knows when sessions have been cleared for the current user (so tests can work), remove unnecessary post body param * test revokeAllSessions to verify it works when there are multiple sessions for the given user --- .../src/action_types/users.js | 4 ++ .../mattermost-redux/src/actions/users.js | 28 +++++++++++++ .../mattermost-redux/src/client/client4.js | 7 ++++ .../src/reducers/entities/users.js | 7 ++++ .../src/reducers/requests/users.js | 11 +++++ .../test/actions/users.test.js | 40 +++++++++++++++++++ 6 files changed, 97 insertions(+) diff --git a/packages/mattermost-redux/src/action_types/users.js b/packages/mattermost-redux/src/action_types/users.js index 0797c7d9126f..c60f697811b4 100644 --- a/packages/mattermost-redux/src/action_types/users.js +++ b/packages/mattermost-redux/src/action_types/users.js @@ -68,6 +68,10 @@ export default keyMirror({ REVOKE_SESSION_SUCCESS: null, REVOKE_SESSION_FAILURE: null, + REVOKE_ALL_USER_SESSIONS_REQUEST: null, + REVOKE_ALL_USER_SESSIONS_SUCCESS: null, + REVOKE_ALL_USER_SESSIONS_FAILURE: null, + AUDITS_REQUEST: null, AUDITS_SUCCESS: null, AUDITS_FAILURE: null, diff --git a/packages/mattermost-redux/src/actions/users.js b/packages/mattermost-redux/src/actions/users.js index 2a8a271dd8ac..f3c229a0728b 100644 --- a/packages/mattermost-redux/src/actions/users.js +++ b/packages/mattermost-redux/src/actions/users.js @@ -27,6 +27,7 @@ import { } from './preferences'; import {getConfig} from 'selectors/entities/general'; +import {getCurrentUserId} from 'selectors/entities/users'; export function checkMfa(loginId) { return async (dispatch, getState) => { @@ -675,6 +676,32 @@ export function revokeSession(userId, sessionId) { }; } +export function revokeAllSessionsForUser(userId) { + return async (dispatch, getState) => { + dispatch({type: UserTypes.REVOKE_ALL_USER_SESSIONS_REQUEST}, getState); + + try { + await Client4.revokeAllSessionsForUser(userId); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch(batchActions([ + {type: UserTypes.REVOKE_ALL_USER_SESSIONS_FAILURE, error}, + logError(error)(dispatch) + ]), getState); + return {error}; + } + const data = {isCurrentUser: userId === getCurrentUserId(getState())}; + dispatch(batchActions([ + { + type: UserTypes.REVOKE_ALL_USER_SESSIONS_SUCCESS, + data + } + ]), getState); + + return {data: true}; + }; +} + export function loadProfilesForDirect() { return async (dispatch, getState) => { const state = getState(); @@ -1362,6 +1389,7 @@ export default { getSessions, loadProfilesForDirect, revokeSession, + revokeAllSessionsForUser, getUserAudits, searchProfiles, startPeriodicStatusUpdates, diff --git a/packages/mattermost-redux/src/client/client4.js b/packages/mattermost-redux/src/client/client4.js index f88f9b448fac..efbd596757aa 100644 --- a/packages/mattermost-redux/src/client/client4.js +++ b/packages/mattermost-redux/src/client/client4.js @@ -583,6 +583,13 @@ export default class Client4 { ); }; + revokeAllSessionsForUser = async (userId) => { + return this.doFetch( + `${this.getUserRoute(userId)}/sessions/revoke/all`, + {method: 'post'} + ); + }; + getUserAudits = async (userId, page = 0, perPage = PER_PAGE_DEFAULT) => { return this.doFetch( `${this.getUserRoute(userId)}/audits${buildQueryString({page, per_page: perPage})}`, diff --git a/packages/mattermost-redux/src/reducers/entities/users.js b/packages/mattermost-redux/src/reducers/entities/users.js index 30cf30bfe4f0..6a5b9ba67c06 100644 --- a/packages/mattermost-redux/src/reducers/entities/users.js +++ b/packages/mattermost-redux/src/reducers/entities/users.js @@ -108,6 +108,13 @@ function mySessions(state = [], action) { return state; } + + case UserTypes.REVOKE_ALL_USER_SESSIONS_SUCCESS: + if (action.data.isCurrentUser === true) { + return []; + } + return state; + case UserTypes.LOGOUT_SUCCESS: return []; diff --git a/packages/mattermost-redux/src/reducers/requests/users.js b/packages/mattermost-redux/src/reducers/requests/users.js index 23bfe83fce49..353ddb262406 100644 --- a/packages/mattermost-redux/src/reducers/requests/users.js +++ b/packages/mattermost-redux/src/reducers/requests/users.js @@ -204,6 +204,16 @@ function revokeSession(state = initialRequestState(), action) { ); } +function revokeAllSessionsForUser(state = initialRequestState(), action) { + return handleRequest( + UserTypes.REVOKE_ALL_USER_SESSIONS_REQUEST, + UserTypes.REVOKE_ALL_USER_SESSIONS_SUCCESS, + UserTypes.REVOKE_ALL_USER_SESSIONS_FAILURE, + state, + action + ); +} + function getAudits(state = initialRequestState(), action) { return handleRequest( UserTypes.AUDITS_REQUEST, @@ -363,6 +373,7 @@ export default combineReducers({ setStatus, getSessions, revokeSession, + revokeAllSessionsForUser, getAudits, autocompleteUsers, searchProfiles, diff --git a/packages/mattermost-redux/test/actions/users.test.js b/packages/mattermost-redux/test/actions/users.test.js index 27da48bcf1ba..f99a01b73b16 100644 --- a/packages/mattermost-redux/test/actions/users.test.js +++ b/packages/mattermost-redux/test/actions/users.test.js @@ -522,6 +522,46 @@ describe('Actions.Users', () => { } }); + it('revokeAllSessionsForCurrentUser', async () => { + const user = TestHelper.basicUser; + await TestHelper.basicClient4.logout(); + let sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + + await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState); + await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); + + await Actions.getSessions(user.id)(store.dispatch, store.getState); + + const sessionsRequest = store.getState().requests.users.getSessions; + + if (sessionsRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(sessionsRequest.error)); + } + + sessions = store.getState().entities.users.mySessions; + assert.ok(sessions.length > 1); + + await Actions.revokeAllSessionsForUser(user.id)(store.dispatch, store.getState); + + const revokeRequest = store.getState().requests.users.revokeAllSessionsForUser; + if (revokeRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(revokeRequest.error)); + } + + await Actions.getProfiles(0)(store.dispatch, store.getState); + + const logoutRequest = store.getState().requests.users.logout; + if (logoutRequest.status === RequestStatus.FAILURE) { + throw new Error(JSON.stringify(logoutRequest.error)); + } + + sessions = store.getState().entities.users.mySessions; + + assert.strictEqual(sessions.length, 0); + }); + it('getUserAudits', async () => { await TestHelper.basicClient4.login(TestHelper.basicUser.email, 'password1'); await Actions.getUserAudits(TestHelper.basicUser.id)(store.dispatch, store.getState);