mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
e8fe5abc82
commit
25d8e053be
15 changed files with 79 additions and 61 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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('/')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
sessionKeys,
|
stayLoggedIn && req?.sessionID
|
||||||
k => !Array.from(retain).includes(k)
|
? _.without(
|
||||||
)
|
sessionKeys,
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -37,15 +37,20 @@ 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(
|
||||||
if (error) {
|
{ _id: userId },
|
||||||
console.log(`Failed to clear sessions for ${userId}`, error)
|
null,
|
||||||
processLogger.failedClear.push(userId)
|
null,
|
||||||
} else {
|
error => {
|
||||||
processLogger.success.push(userId)
|
if (error) {
|
||||||
|
console.log(`Failed to clear sessions for ${userId}`, error)
|
||||||
|
processLogger.failedClear.push(userId)
|
||||||
|
} else {
|
||||||
|
processLogger.success.push(userId)
|
||||||
|
}
|
||||||
|
return callback()
|
||||||
}
|
}
|
||||||
return callback()
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, []) },
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue