[web] Update revokeAllUserSessions and rename it to removeSessionsFromRedis (#18360)

* Fix `revokeAllUserSessions` call in `_cleanupUser`

The user object should be passed, not the _id

* Change `revokeAllUserSessions` signature, take `req` and `stayLoggedIn` arguments

* Update uses of `revokeAllUserSessions`

* Fix promisified `revokeAllUserSessions` args

* Update tests

* Destroy or Regenerate the session in the end of `revokeAllUserSessions`

Per https://github.com/overleaf/internal/issues/17036#issuecomment-1938398570

* Revert "Destroy or Regenerate the session in the end of `revokeAllUserSessions`"

This reverts commit fe30734dbe45b27d2931d2e43a711d591bb85787.

* Rename `revokeAllUserSessions` to `removeSessionsFromRedis`

* Fixup tests

* Fix: add optional chaining in `req.sessionID` (!!)

GitOrigin-RevId: d41676bf00f463230af495e09c65fb9ee521f49f
This commit is contained in:
Antoine Clausse 2024-05-17 12:15:14 +02:00 committed by Copybot
parent e8fe5abc82
commit 25d8e053be
15 changed files with 79 additions and 61 deletions

View file

@ -57,10 +57,7 @@ async function setNewUserPassword(req, res, next) {
message: req.i18n.translate('error_performing_request'), message: req.i18n.translate('error_performing_request'),
}) })
} }
await UserSessionsManager.promises.revokeAllUserSessions( await UserSessionsManager.promises.removeSessionsFromRedis({ _id: userId })
{ _id: userId },
[]
)
await UserUpdater.promises.removeReconfirmFlag(userId) await UserUpdater.promises.removeReconfirmFlag(userId)
if (!req.session.doLoginAfterPasswordReset) { if (!req.session.doLoginAfterPasswordReset) {
return res.sendStatus(200) return res.sendStatus(200)

View file

@ -130,9 +130,9 @@ async function changePassword(req, res, next) {
// no need to wait, errors are logged and not passed back // no need to wait, errors are logged and not passed back
_sendSecurityAlertPasswordChanged(user) _sendSecurityAlertPasswordChanged(user)
await UserSessionsManager.promises.revokeAllUserSessions(user, [ await UserSessionsManager.promises.removeSessionsFromRedis(user, req, {
req.sessionID, stayLoggedIn: true,
]) })
await OneTimeTokenHandler.promises.expireAllTokensForUser( await OneTimeTokenHandler.promises.expireAllTokensForUser(
userId.toString(), userId.toString(),
@ -162,9 +162,9 @@ async function clearSessions(req, res, next) {
req.ip, req.ip,
{ sessions } { sessions }
) )
await UserSessionsManager.promises.revokeAllUserSessions(user, [ await UserSessionsManager.promises.removeSessionsFromRedis(user, req, {
req.sessionID, stayLoggedIn: true,
]) })
await _sendSecurityAlertClearedSessions(user) await _sendSecurityAlertClearedSessions(user)

View file

@ -152,7 +152,7 @@ async function _createDeletedUser(user, options) {
} }
async function _cleanupUser(user) { async function _cleanupUser(user) {
await UserSessionsManager.promises.revokeAllUserSessions(user._id, []) await UserSessionsManager.promises.removeSessionsFromRedis(user)
await NewsletterManager.promises.unsubscribe(user, { delete: true }) await NewsletterManager.promises.unsubscribe(user, { delete: true })
await SubscriptionHandler.promises.cancelSubscription(user) await SubscriptionHandler.promises.cancelSubscription(user)
await InstitutionsAPI.promises.deleteAffiliations(user._id) await InstitutionsAPI.promises.deleteAffiliations(user._id)

View file

@ -510,9 +510,10 @@ const UserEmailsController = {
} }
SessionManager.setInSessionUser(req.session, { email }) SessionManager.setInSessionUser(req.session, { email })
const user = SessionManager.getSessionUser(req.session) const user = SessionManager.getSessionUser(req.session)
UserSessionsManager.revokeAllUserSessions( UserSessionsManager.removeSessionsFromRedis(
user, user,
[req.sessionID], req,
{ stayLoggedIn: true },
err => { err => {
if (err) if (err)
logger.warn( logger.warn(

View file

@ -64,8 +64,11 @@ async function settingsPage(req, res) {
const user = await UserGetter.promises.getUser(userId) const user = await UserGetter.promises.getUser(userId)
if (!user) { if (!user) {
// The user has just deleted their account. // The user has just deleted their account.
return UserSessionsManager.revokeAllUserSessions({ _id: userId }, [], () => return UserSessionsManager.removeSessionsFromRedis(
res.redirect('/') { _id: userId },
req,
{ stayLoggedIn: false },
() => res.redirect('/')
) )
} }

View file

@ -126,11 +126,14 @@ const UserSessionsManager = {
}) })
}, },
revokeAllUserSessions(user, retain, callback) { /**
if (!retain) { * @param {{_id: string}} user
retain = [] * @param {(import('express').Request & {sessionID?: string}) | undefined} req - the request object. Can be omitted if stayLoggedIn is false.
} * @param {{stayLoggedIn: boolean}?} options
retain = retain.map(i => UserSessionsManager._sessionKey(i)) * @param {(err: Error | null, data?: unknown) => void} callback
*/
removeSessionsFromRedis(user, req, options, callback) {
const stayLoggedIn = options?.stayLoggedIn ?? false
if (!user) { if (!user) {
return callback(null) return callback(null)
} }
@ -143,10 +146,13 @@ const UserSessionsManager = {
}) })
return callback(err) return callback(err)
} }
const keysToDelete = _.filter( const keysToDelete =
stayLoggedIn && req?.sessionID
? _.without(
sessionKeys, sessionKeys,
k => !Array.from(retain).includes(k) UserSessionsManager._sessionKey(req.sessionID)
) )
: sessionKeys
if (keysToDelete.length === 0) { if (keysToDelete.length === 0) {
logger.debug( logger.debug(
{ userId: user._id }, { userId: user._id },
@ -242,7 +248,8 @@ const UserSessionsManager = {
UserSessionsManager.promises = { UserSessionsManager.promises = {
getAllUserSessions: promisify(UserSessionsManager.getAllUserSessions), getAllUserSessions: promisify(UserSessionsManager.getAllUserSessions),
revokeAllUserSessions: promisify(UserSessionsManager.revokeAllUserSessions), removeSessionsFromRedis: (user, req = null, options = null) =>
promisify(UserSessionsManager.removeSessionsFromRedis)(user, req, options),
untrackSession: promisify(UserSessionsManager.untrackSession), untrackSession: promisify(UserSessionsManager.untrackSession),
} }

View file

@ -142,9 +142,9 @@ async function doMigration(emails) {
`Updating user ${userWithEmail._id} email "${oldEmail}" to "${newEmail}"\n` `Updating user ${userWithEmail._id} email "${oldEmail}" to "${newEmail}"\n`
) )
try { try {
await UserSessionsManager.promises.revokeAllUserSessions( // log out all the user's sessions before changing the email address
userWithEmail, await UserSessionsManager.promises.removeSessionsFromRedis(
[] // log out all the user's sessions before changing the email address userWithEmail
) )
await UserUpdater.promises.migrateDefaultEmailAddress( await UserUpdater.promises.migrateDefaultEmailAddress(

View file

@ -77,7 +77,7 @@ async function main() {
{ $set: { staffAccess: FULL_STAFF_ACCESS } } { $set: { staffAccess: FULL_STAFF_ACCESS } }
) )
if (!KEEP_SESSIONS) { if (!KEEP_SESSIONS) {
await UserSessionsManager.promises.revokeAllUserSessions(user, []) await UserSessionsManager.promises.removeSessionsFromRedis(user)
} }
} }
} else { } else {

View file

@ -37,7 +37,11 @@ function _handleUser(userId, callback) {
processLogger.failedSet.push(userId) processLogger.failedSet.push(userId)
return callback() return callback()
} else { } else {
UserSessionsManager.revokeAllUserSessions({ _id: userId }, [], error => { UserSessionsManager.removeSessionsFromRedis(
{ _id: userId },
null,
null,
error => {
if (error) { if (error) {
console.log(`Failed to clear sessions for ${userId}`, error) console.log(`Failed to clear sessions for ${userId}`, error)
processLogger.failedClear.push(userId) processLogger.failedClear.push(userId)
@ -45,7 +49,8 @@ function _handleUser(userId, callback) {
processLogger.success.push(userId) processLogger.success.push(userId)
} }
return callback() return callback()
}) }
)
} }
}) })
} }

View file

@ -103,7 +103,7 @@ describe('AuthenticationController', function () {
'../User/UserSessionsManager': (this.UserSessionsManager = { '../User/UserSessionsManager': (this.UserSessionsManager = {
trackSession: sinon.stub(), trackSession: sinon.stub(),
untrackSession: sinon.stub(), untrackSession: sinon.stub(),
revokeAllUserSessions: sinon.stub().yields(null), removeSessionsFromRedis: sinon.stub().yields(null),
}), }),
'../../infrastructure/Modules': (this.Modules = { '../../infrastructure/Modules': (this.Modules = {
hooks: { fire: sinon.stub().yields(null, []) }, hooks: { fire: sinon.stub().yields(null, []) },

View file

@ -50,7 +50,7 @@ describe('PasswordResetController', function () {
} }
this.UserSessionsManager = { this.UserSessionsManager = {
promises: { promises: {
revokeAllUserSessions: sinon.stub().resolves(), removeSessionsFromRedis: sinon.stub().resolves(),
}, },
} }
this.UserUpdater = { this.UserUpdater = {
@ -264,7 +264,7 @@ describe('PasswordResetController', function () {
it('should clear sessions', function (done) { it('should clear sessions', function (done) {
this.res.sendStatus = code => { this.res.sendStatus = code => {
this.UserSessionsManager.promises.revokeAllUserSessions.callCount.should.equal( this.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal(
1 1
) )
done() done()

View file

@ -91,7 +91,7 @@ describe('UserController', function () {
this.UserSessionsManager = { this.UserSessionsManager = {
promises: { promises: {
getAllUserSessions: sinon.stub().resolves(), getAllUserSessions: sinon.stub().resolves(),
revokeAllUserSessions: sinon.stub().resolves(), removeSessionsFromRedis: sinon.stub().resolves(),
untrackSession: sinon.stub().resolves(), untrackSession: sinon.stub().resolves(),
}, },
} }
@ -603,9 +603,9 @@ describe('UserController', function () {
describe('clearSessions', function () { describe('clearSessions', function () {
describe('success', function () { describe('success', function () {
it('should call revokeAllUserSessions', function (done) { it('should call removeSessionsFromRedis', function (done) {
this.res.sendStatus.callsFake(() => { this.res.sendStatus.callsFake(() => {
this.UserSessionsManager.promises.revokeAllUserSessions.should.have this.UserSessionsManager.promises.removeSessionsFromRedis.should.have
.been.calledOnce .been.calledOnce
done() done()
}) })
@ -662,9 +662,9 @@ describe('UserController', function () {
}) })
}) })
describe('when revokeAllUserSessions produces an error', function () { describe('when removeSessionsFromRedis produces an error', function () {
it('should call next with an error', function (done) { it('should call next with an error', function (done) {
this.UserSessionsManager.promises.revokeAllUserSessions.rejects( this.UserSessionsManager.promises.removeSessionsFromRedis.rejects(
new Error('woops') new Error('woops')
) )
this.UserController.clearSessions(this.req, this.res, error => { this.UserController.clearSessions(this.req, this.res, error => {

View file

@ -75,7 +75,7 @@ describe('UserDeleter', function () {
this.UserSessionsManager = { this.UserSessionsManager = {
promises: { promises: {
revokeAllUserSessions: sinon.stub().resolves(), removeSessionsFromRedis: sinon.stub().resolves(),
}, },
} }
@ -250,8 +250,8 @@ describe('UserDeleter', function () {
ipAddress: this.ipAddress, ipAddress: this.ipAddress,
}) })
expect( expect(
this.UserSessionsManager.promises.revokeAllUserSessions this.UserSessionsManager.promises.removeSessionsFromRedis
).to.have.been.calledWith(this.userId, []) ).to.have.been.calledWith(this.user)
}) })
it('should remove user from group subscriptions', async function () { it('should remove user from group subscriptions', async function () {

View file

@ -38,7 +38,7 @@ describe('UserEmailsController', function () {
hasFeature: sinon.stub(), hasFeature: sinon.stub(),
} }
this.UserSessionsManager = { this.UserSessionsManager = {
revokeAllUserSessions: sinon.stub().yields(), removeSessionsFromRedis: sinon.stub().yields(),
} }
this.UserUpdater = { this.UserUpdater = {
addEmailAddress: sinon.stub(), addEmailAddress: sinon.stub(),
@ -565,8 +565,8 @@ describe('UserEmailsController', function () {
this.res.callback = () => { this.res.callback = () => {
expect( expect(
this.UserSessionsManager.revokeAllUserSessions this.UserSessionsManager.removeSessionsFromRedis
).to.have.been.calledWith(this.user, [this.req.sessionID]) ).to.have.been.calledWith(this.user, this.req)
done() done()
} }
@ -576,7 +576,7 @@ describe('UserEmailsController', function () {
it('handles error from revoking sessions and returns 200', function (done) { it('handles error from revoking sessions and returns 200', function (done) {
this.UserUpdater.setDefaultEmailAddress.yields() this.UserUpdater.setDefaultEmailAddress.yields()
const redisError = new Error('redis error') const redisError = new Error('redis error')
this.UserSessionsManager.revokeAllUserSessions = sinon this.UserSessionsManager.removeSessionsFromRedis = sinon
.stub() .stub()
.yields(redisError) .yields(redisError)

View file

@ -340,17 +340,19 @@ describe('UserSessionsManager', function () {
}) })
}) })
describe('revokeAllUserSessions', function () { describe('removeSessionsFromRedis', function () {
beforeEach(function () { beforeEach(function () {
this.sessionKeys = ['sess:one', 'sess:two'] this.sessionKeys = ['sess:one', 'sess:two']
this.retain = [] this.currentSessionID = undefined
this.stayLoggedIn = false
this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) this.rclient.smembers.callsArgWith(1, null, this.sessionKeys)
this.rclient.del = sinon.stub().callsArgWith(1, null) this.rclient.del = sinon.stub().callsArgWith(1, null)
this.rclient.srem = sinon.stub().callsArgWith(2, null) this.rclient.srem = sinon.stub().callsArgWith(2, null)
return (this.call = callback => { return (this.call = callback => {
return this.UserSessionsManager.revokeAllUserSessions( return this.UserSessionsManager.removeSessionsFromRedis(
this.user, this.user,
this.retain, { sessionID: this.currentSessionID },
{ stayLoggedIn: this.stayLoggedIn },
callback callback
) )
}) })
@ -387,13 +389,15 @@ describe('UserSessionsManager', function () {
describe('when a session is retained', function () { describe('when a session is retained', function () {
beforeEach(function () { beforeEach(function () {
this.sessionKeys = ['sess:one', 'sess:two', 'sess:three', 'sess:four'] this.sessionKeys = ['sess:one', 'sess:two', 'sess:three', 'sess:four']
this.retain = ['two'] this.currentSessionID = 'two'
this.stayLoggedIn = true
this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) this.rclient.smembers.callsArgWith(1, null, this.sessionKeys)
this.rclient.del = sinon.stub().callsArgWith(1, null) this.rclient.del = sinon.stub().callsArgWith(1, null)
return (this.call = callback => { return (this.call = callback => {
return this.UserSessionsManager.revokeAllUserSessions( return this.UserSessionsManager.removeSessionsFromRedis(
this.user, this.user,
this.retain, { sessionID: this.currentSessionID },
{ stayLoggedIn: this.stayLoggedIn },
callback callback
) )
}) })
@ -457,9 +461,10 @@ describe('UserSessionsManager', function () {
describe('when no user is supplied', function () { describe('when no user is supplied', function () {
beforeEach(function () { beforeEach(function () {
return (this.call = callback => { return (this.call = callback => {
return this.UserSessionsManager.revokeAllUserSessions( return this.UserSessionsManager.removeSessionsFromRedis(
null, null,
this.retain, { sessionID: this.currentSessionID },
{ stayLoggedIn: this.stayLoggedIn },
callback callback
) )
}) })