mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-30 20:31:34 +02:00
* [web] fix caching of session.analyticsId * [web] disable analyticsIdMiddleware tests for Server Pro/CE GitOrigin-RevId: 2acf76f937adabd62b1e9f876a858211ef7a13c6
432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
import { expect } from 'chai'
|
|
import { setTimeout } from 'node:timers/promises'
|
|
import UserHelper from './helpers/User.mjs'
|
|
import redis from './helpers/redis.mjs'
|
|
import Metrics from './helpers/metrics.mjs'
|
|
import UserSessionsRedis from '../../../app/src/Features/User/UserSessionsRedis.mjs'
|
|
import UserAnalyticsIdCache from '../../../app/src/Features/Analytics/UserAnalyticsIdCache.mjs'
|
|
import Features from '../../../app/src/infrastructure/Features.mjs'
|
|
|
|
const rclient = UserSessionsRedis.client()
|
|
|
|
const UserPromises = UserHelper.promises
|
|
|
|
describe('Sessions', function () {
|
|
beforeEach(async function () {
|
|
this.timeout(20000)
|
|
this.user1 = new UserPromises()
|
|
this.site_admin = new UserPromises({ email: 'admin@example.com' })
|
|
await this.user1.login()
|
|
await this.user1.logout()
|
|
})
|
|
|
|
describe('one session', function () {
|
|
it('should have one session in UserSessions set', async function () {
|
|
await redis.clearUserSessions(this.user1)
|
|
|
|
// login, should add session to set
|
|
await this.user1.login()
|
|
|
|
const sessions = await redis.getUserSessions(this.user1)
|
|
expect(sessions.length).to.equal(1)
|
|
expect(sessions[0].slice(0, 5)).to.equal('sess:')
|
|
|
|
// should be able to access project list page
|
|
const statusCode = await this.user1.getProjectListPage()
|
|
expect(statusCode).to.equal(200)
|
|
|
|
// logout, should remove session from set
|
|
await this.user1.logout()
|
|
const sessionsAfterLogout = await redis.getUserSessions(this.user1)
|
|
expect(sessionsAfterLogout.length).to.equal(0)
|
|
})
|
|
|
|
it('should update audit log on logout', async function () {
|
|
await redis.clearUserSessions(this.user1)
|
|
|
|
// login
|
|
await this.user1.login()
|
|
|
|
// logout, should add logout audit log entry (happens in background)
|
|
await this.user1.logout()
|
|
|
|
// poll for audit log entry since it's written in the background
|
|
const findAuditLogEntry = async () => {
|
|
for (let attempts = 0; attempts < 10; attempts++) {
|
|
const auditLog = await this.user1.getAuditLogWithoutNoise()
|
|
|
|
const logoutEntries = auditLog.filter(
|
|
entry => entry.operation === 'logout'
|
|
)
|
|
|
|
// If we found the logout entry, we're done
|
|
if (logoutEntries.length > 0) {
|
|
return logoutEntries
|
|
}
|
|
|
|
await setTimeout(25)
|
|
}
|
|
|
|
throw new Error('Logout audit log entry not found after 10 attempts')
|
|
}
|
|
|
|
const logoutEntries = await findAuditLogEntry()
|
|
expect(logoutEntries.length).to.be.greaterThan(0)
|
|
const lastLogout = logoutEntries[logoutEntries.length - 1]
|
|
expect(lastLogout.operation).to.equal('logout')
|
|
expect(lastLogout.ipAddress).to.exist
|
|
expect(lastLogout.initiatorId).to.exist
|
|
expect(lastLogout.timestamp).to.exist
|
|
})
|
|
})
|
|
|
|
describe('two sessions', function () {
|
|
beforeEach(function () {
|
|
// set up second session for this user
|
|
this.user2 = new UserPromises()
|
|
this.user2.email = this.user1.email
|
|
this.user2.emails = this.user1.emails
|
|
this.user2.password = this.user1.password
|
|
})
|
|
|
|
it('should have two sessions in UserSessions set', async function () {
|
|
await redis.clearUserSessions(this.user1)
|
|
|
|
// login, should add session to set
|
|
await this.user1.login()
|
|
|
|
const sessions1 = await redis.getUserSessions(this.user1)
|
|
expect(sessions1.length).to.equal(1)
|
|
expect(sessions1[0].slice(0, 5)).to.equal('sess:')
|
|
|
|
// login again, should add the second session to set
|
|
await this.user2.login()
|
|
|
|
const sessions2 = await redis.getUserSessions(this.user1)
|
|
expect(sessions2.length).to.equal(2)
|
|
expect(sessions2[0].slice(0, 5)).to.equal('sess:')
|
|
expect(sessions2[1].slice(0, 5)).to.equal('sess:')
|
|
|
|
// both should be able to access project list page
|
|
const statusCode1 = await this.user1.getProjectListPage()
|
|
expect(statusCode1).to.equal(200)
|
|
const statusCode2 = await this.user2.getProjectListPage()
|
|
expect(statusCode2).to.equal(200)
|
|
|
|
// logout first session, should remove session from set
|
|
await this.user1.logout()
|
|
|
|
const sessions3 = await redis.getUserSessions(this.user1)
|
|
expect(sessions3.length).to.equal(1)
|
|
|
|
// first session should not have access to project list page
|
|
const statusCode3 = await this.user1.getProjectListPage()
|
|
expect(statusCode3).to.equal(302)
|
|
|
|
// second session should still have access to settings
|
|
const statusCode4 = await this.user2.getProjectListPage()
|
|
expect(statusCode4).to.equal(200)
|
|
|
|
// logout second session, should remove last session from set
|
|
await this.user2.logout()
|
|
|
|
const sessions4 = await redis.getUserSessions(this.user1)
|
|
expect(sessions4.length).to.equal(0)
|
|
|
|
// second session should not have access to project list page
|
|
const statusCode5 = await this.user2.getProjectListPage()
|
|
expect(statusCode5).to.equal(302)
|
|
})
|
|
})
|
|
|
|
describe('three sessions, password reset', function () {
|
|
beforeEach(function () {
|
|
// set up second session for this user
|
|
this.user2 = new UserPromises()
|
|
this.user2.email = this.user1.email
|
|
this.user2.emails = this.user1.emails
|
|
this.user2.password = this.user1.password
|
|
this.user3 = new UserPromises()
|
|
this.user3.email = this.user1.email
|
|
this.user3.emails = this.user1.emails
|
|
this.user3.password = this.user1.password
|
|
})
|
|
|
|
it('should erase both sessions when password is reset', async function () {
|
|
await redis.clearUserSessions(this.user1)
|
|
|
|
// login, should add session to set
|
|
await this.user1.login()
|
|
|
|
const sessions1 = await redis.getUserSessions(this.user1)
|
|
expect(sessions1.length).to.equal(1)
|
|
expect(sessions1[0].slice(0, 5)).to.equal('sess:')
|
|
|
|
// login again, should add the second session to set
|
|
await this.user2.login()
|
|
|
|
const sessions2 = await redis.getUserSessions(this.user1)
|
|
expect(sessions2.length).to.equal(2)
|
|
expect(sessions2[0].slice(0, 5)).to.equal('sess:')
|
|
expect(sessions2[1].slice(0, 5)).to.equal('sess:')
|
|
|
|
// login third session, should add the second session to set
|
|
await this.user3.login()
|
|
|
|
const sessions3 = await redis.getUserSessions(this.user1)
|
|
expect(sessions3.length).to.equal(3)
|
|
expect(sessions3[0].slice(0, 5)).to.equal('sess:')
|
|
expect(sessions3[1].slice(0, 5)).to.equal('sess:')
|
|
|
|
// password reset from second session, should erase two of the three sessions
|
|
await this.user2.changePassword(`password${Date.now()}`)
|
|
|
|
const sessions4 = await redis.getUserSessions(this.user2)
|
|
expect(sessions4.length).to.equal(1)
|
|
|
|
// users one and three should not be able to access project list page
|
|
const statusCode1 = await this.user1.getProjectListPage()
|
|
expect(statusCode1).to.equal(302)
|
|
const statusCode3 = await this.user3.getProjectListPage()
|
|
expect(statusCode3).to.equal(302)
|
|
|
|
// user two should still be logged in, and able to access project list page
|
|
const statusCode2 = await this.user2.getProjectListPage()
|
|
expect(statusCode2).to.equal(200)
|
|
|
|
// logout second session, should remove last session from set
|
|
await this.user2.logout()
|
|
|
|
const sessions5 = await redis.getUserSessions(this.user1)
|
|
expect(sessions5.length).to.equal(0)
|
|
})
|
|
})
|
|
|
|
describe('three sessions, sessions page', function () {
|
|
beforeEach(async function () {
|
|
// set up second session for this user
|
|
this.user2 = new UserPromises()
|
|
this.user2.email = this.user1.email
|
|
this.user2.emails = this.user1.emails
|
|
this.user2.password = this.user1.password
|
|
this.user3 = new UserPromises()
|
|
this.user3.email = this.user1.email
|
|
this.user3.emails = this.user1.emails
|
|
this.user3.password = this.user1.password
|
|
await this.user2.login()
|
|
})
|
|
|
|
it('should allow the user to erase the other two sessions', async function () {
|
|
await redis.clearUserSessions(this.user1)
|
|
|
|
// login, should add session to set
|
|
await this.user1.login()
|
|
|
|
const sessions1 = await redis.getUserSessions(this.user1)
|
|
expect(sessions1.length).to.equal(1)
|
|
expect(sessions1[0].slice(0, 5)).to.equal('sess:')
|
|
|
|
// login again, should add the second session to set
|
|
await this.user2.login()
|
|
|
|
const sessions2 = await redis.getUserSessions(this.user1)
|
|
expect(sessions2.length).to.equal(2)
|
|
expect(sessions2[0].slice(0, 5)).to.equal('sess:')
|
|
expect(sessions2[1].slice(0, 5)).to.equal('sess:')
|
|
|
|
// login third session, should add the second session to set
|
|
await this.user3.login()
|
|
|
|
const sessions3 = await redis.getUserSessions(this.user1)
|
|
expect(sessions3.length).to.equal(3)
|
|
expect(sessions3[0].slice(0, 5)).to.equal('sess:')
|
|
expect(sessions3[1].slice(0, 5)).to.equal('sess:')
|
|
|
|
// check the sessions page
|
|
const { response: sessionsPageResponse } = await this.user2.doRequest(
|
|
'GET',
|
|
'/user/sessions'
|
|
)
|
|
expect(sessionsPageResponse.statusCode).to.equal(200)
|
|
|
|
// clear sessions from second session, should erase two of the three sessions
|
|
await this.user2.getCsrfToken()
|
|
await this.user2.doRequest('POST', '/user/sessions/clear')
|
|
const sessions4 = await redis.getUserSessions(this.user2)
|
|
expect(sessions4.length).to.equal(1)
|
|
|
|
// users one and three should not be able to access project list page
|
|
const statusCode1 = await this.user1.getProjectListPage()
|
|
expect(statusCode1).to.equal(302)
|
|
const statusCode3 = await this.user3.getProjectListPage()
|
|
expect(statusCode3).to.equal(302)
|
|
|
|
// user two should still be logged in, and able to access project list page
|
|
const statusCode2 = await this.user2.getProjectListPage()
|
|
expect(statusCode2).to.equal(200)
|
|
|
|
// logout second session, should remove last session from set
|
|
await this.user2.logout()
|
|
const sessions5 = await redis.getUserSessions(this.user1)
|
|
expect(sessions5.length).to.equal(0)
|
|
|
|
// the user audit log should have been updated
|
|
const auditLog = await this.user1.getAuditLogWithoutNoise()
|
|
expect(auditLog).to.exist
|
|
|
|
// find the clear-sessions entry
|
|
const clearSessionsEntries = auditLog.filter(
|
|
entry => entry.operation === 'clear-sessions'
|
|
)
|
|
expect(clearSessionsEntries.length).to.equal(1)
|
|
expect(clearSessionsEntries[0].operation).to.equal('clear-sessions')
|
|
expect(clearSessionsEntries[0].ipAddress).to.exist
|
|
expect(clearSessionsEntries[0].initiatorId).to.exist
|
|
expect(clearSessionsEntries[0].timestamp).to.exist
|
|
expect(clearSessionsEntries[0].info.sessions.length).to.equal(2)
|
|
})
|
|
})
|
|
|
|
describe('validationToken', function () {
|
|
const User = UserHelper.promises
|
|
|
|
async function tryWithValidationToken(validationToken) {
|
|
const user = new User()
|
|
await user.login()
|
|
|
|
await checkSessionIsValid(user)
|
|
|
|
const [, sid] = user.sessionCookie().value.match(/^s:(.+?)\./)
|
|
const key = `sess:${sid}`
|
|
const sess = JSON.parse(await rclient.get(key))
|
|
|
|
expect(sess.validationToken).to.equal('v1:' + sid.slice(-4))
|
|
|
|
sess.validationToken = validationToken
|
|
await rclient.set(key, JSON.stringify(sess))
|
|
|
|
{
|
|
// The current code destroys the session and throws an error/500.
|
|
// Check for login redirect on page reload.
|
|
await user.doRequest('GET', '/project')
|
|
|
|
const { response } = await user.doRequest('GET', '/project')
|
|
expect(response.statusCode).to.equal(302)
|
|
expect(response.headers.location).to.equal('/login?')
|
|
}
|
|
}
|
|
|
|
async function getOtherUsersValidationToken() {
|
|
const otherUser = new User()
|
|
await otherUser.login()
|
|
await checkSessionIsValid(otherUser)
|
|
const { validationToken } = await otherUser.getSession()
|
|
expect(validationToken).to.match(/^v1:.{4}$/)
|
|
return validationToken
|
|
}
|
|
async function checkSessionIsValid(user) {
|
|
const { response } = await user.doRequest('GET', '/project')
|
|
expect(response.statusCode).to.equal(200)
|
|
}
|
|
|
|
it('should reject the redis value when missing', async function () {
|
|
await tryWithValidationToken(undefined)
|
|
})
|
|
it('should reject the redis value when empty', async function () {
|
|
await tryWithValidationToken('')
|
|
})
|
|
it('should reject the redis value when out of sync', async function () {
|
|
await tryWithValidationToken(await getOtherUsersValidationToken())
|
|
})
|
|
it('should ignore overwrites in app code', async function () {
|
|
const otherUsersValidationToken = await getOtherUsersValidationToken()
|
|
|
|
const user = new User()
|
|
await user.login()
|
|
await checkSessionIsValid(user)
|
|
|
|
const { validationToken: token1 } = await user.getSession()
|
|
const allowedUpdateValue = 'allowed-update-value'
|
|
await user.setInSession({
|
|
validationToken: otherUsersValidationToken,
|
|
// also update another field to check that the write operation went through
|
|
allowedUpdate: allowedUpdateValue,
|
|
})
|
|
const { validationToken: token2, allowedUpdate } = await user.getSession()
|
|
expect(allowedUpdate).to.equal(allowedUpdateValue)
|
|
expect(token1).to.equal(token2)
|
|
|
|
await checkSessionIsValid(user)
|
|
})
|
|
})
|
|
|
|
describe('analyticsIdMiddleware', function () {
|
|
async function getCacheMetric() {
|
|
const hit = await Metrics.promises.getMetric(
|
|
s => s.includes('analyticsIdMiddleware') && s.includes('hit')
|
|
)
|
|
const miss = await Metrics.promises.getMetric(
|
|
s => s.includes('analyticsIdMiddleware') && s.includes('miss')
|
|
)
|
|
return { hit, miss }
|
|
}
|
|
let usersAnalyticsId
|
|
beforeEach(function () {
|
|
if (!Features.hasFeature('saas')) {
|
|
this.skip()
|
|
}
|
|
usersAnalyticsId = this.user1.analyticsId
|
|
expect(usersAnalyticsId).to.match(/^[0-9a-f-]{36}$/)
|
|
})
|
|
|
|
it('should resolve to the users analyticsId', async function () {
|
|
await this.user1.login()
|
|
const session = await this.user1.getSession()
|
|
expect(session.analyticsId).to.equal(usersAnalyticsId)
|
|
})
|
|
|
|
it('should not lookup the users analyticsId after login', async function () {
|
|
const prev = await getCacheMetric()
|
|
await this.user1.login()
|
|
for (let i = 0; i < 5; i++) {
|
|
await this.user1.doRequest('GET', '/project')
|
|
}
|
|
const current = await getCacheMetric()
|
|
expect(current).to.deep.equal(prev)
|
|
const session = await this.user1.getSession()
|
|
expect(session.analyticsId).to.equal(usersAnalyticsId)
|
|
})
|
|
|
|
it('should lookup the users analyticsId for old session', async function () {
|
|
usersAnalyticsId = this.user1._id
|
|
await this.user1.mongoUpdate({
|
|
$set: { analyticsId: usersAnalyticsId },
|
|
})
|
|
const prev = await getCacheMetric()
|
|
await this.user1.login()
|
|
let session = await this.user1.getSession()
|
|
await this.user1.setInSession({
|
|
analyticsId: null,
|
|
passport: {
|
|
...session.passport,
|
|
user: {
|
|
...session.passport.user,
|
|
analyticsId: null,
|
|
},
|
|
},
|
|
})
|
|
await UserAnalyticsIdCache.reset()
|
|
for (let i = 0; i < 5; i++) {
|
|
await this.user1.doRequest('GET', '/project')
|
|
}
|
|
const current = await getCacheMetric()
|
|
expect(current).to.deep.equal({
|
|
hit: prev.hit,
|
|
miss: prev.miss + 1,
|
|
})
|
|
session = await this.user1.getSession()
|
|
expect(session.analyticsId).to.equal(usersAnalyticsId)
|
|
})
|
|
})
|
|
})
|