mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #13140 from overleaf/jk-real-time-disconnect-link-sharing
[real-time] Disconnect relevant users when link-sharing is turned off GitOrigin-RevId: cf44a30a235717b658a759e8a74ae4d0e5abae47
This commit is contained in:
parent
bb815268f2
commit
d68ed0efdf
13 changed files with 424 additions and 81 deletions
|
@ -40,12 +40,12 @@ module.exports = {
|
||||||
if (!(data && data.project)) {
|
if (!(data && data.project)) {
|
||||||
return callback(new CorruptedJoinProjectResponseError())
|
return callback(new CorruptedJoinProjectResponseError())
|
||||||
}
|
}
|
||||||
callback(
|
const userMetadata = {
|
||||||
null,
|
isRestrictedUser: data.isRestrictedUser,
|
||||||
data.project,
|
isTokenMember: data.isTokenMember,
|
||||||
data.privilegeLevel,
|
isInvitedMember: data.isInvitedMember,
|
||||||
data.isRestrictedUser
|
}
|
||||||
)
|
callback(null, data.project, data.privilegeLevel, userMetadata)
|
||||||
} else if (response.statusCode === 429) {
|
} else if (response.statusCode === 429) {
|
||||||
callback(
|
callback(
|
||||||
new CodedError(
|
new CodedError(
|
||||||
|
|
|
@ -43,7 +43,7 @@ module.exports = WebsocketController = {
|
||||||
WebApiManager.joinProject(
|
WebApiManager.joinProject(
|
||||||
projectId,
|
projectId,
|
||||||
user,
|
user,
|
||||||
function (error, project, privilegeLevel, isRestrictedUser) {
|
function (error, project, privilegeLevel, userMetadata) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,9 @@ module.exports = WebsocketController = {
|
||||||
client.ol_context.connected_time = new Date()
|
client.ol_context.connected_time = new Date()
|
||||||
client.ol_context.signup_date = user.signUpDate
|
client.ol_context.signup_date = user.signUpDate
|
||||||
client.ol_context.login_count = user.loginCount
|
client.ol_context.login_count = user.loginCount
|
||||||
client.ol_context.is_restricted_user = !!isRestrictedUser
|
client.ol_context.is_restricted_user = !!userMetadata.isRestrictedUser
|
||||||
|
client.ol_context.is_token_member = !!userMetadata.isTokenMember
|
||||||
|
client.ol_context.is_invited_member = !!userMetadata.isInvitedMember
|
||||||
|
|
||||||
RoomManager.joinProject(client, projectId, function (err) {
|
RoomManager.joinProject(client, projectId, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -85,7 +87,7 @@ module.exports = WebsocketController = {
|
||||||
projectId,
|
projectId,
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
privilegeLevel,
|
privilegeLevel,
|
||||||
isRestrictedUser,
|
userMetadata,
|
||||||
},
|
},
|
||||||
'user joined project'
|
'user joined project'
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,14 @@ module.exports = WebsocketLoadBalancer = {
|
||||||
if (message?.payload?.includes(userId)) {
|
if (message?.payload?.includes(userId)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
} else if (message?.message === 'project:publicAccessLevel:changed') {
|
||||||
|
const [info] = message.payload
|
||||||
|
if (
|
||||||
|
info.newAccessLevel === 'private' &&
|
||||||
|
!client.ol_context.is_invited_member
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
@ -139,12 +147,7 @@ module.exports = WebsocketLoadBalancer = {
|
||||||
!RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
|
!RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
|
||||||
|
|
||||||
// send messages only to unique clients (due to duplicate entries in io.sockets.clients)
|
// send messages only to unique clients (due to duplicate entries in io.sockets.clients)
|
||||||
const clientList = io.sockets
|
const clientList = io.sockets.clients(message.room_id)
|
||||||
.clients(message.room_id)
|
|
||||||
.filter(
|
|
||||||
client =>
|
|
||||||
!(isRestrictedMessage && client.ol_context.is_restricted_user)
|
|
||||||
)
|
|
||||||
|
|
||||||
// avoid unnecessary work if no clients are connected
|
// avoid unnecessary work if no clients are connected
|
||||||
if (clientList.length === 0) {
|
if (clientList.length === 0) {
|
||||||
|
@ -173,9 +176,14 @@ module.exports = WebsocketLoadBalancer = {
|
||||||
},
|
},
|
||||||
'disconnecting client'
|
'disconnecting client'
|
||||||
)
|
)
|
||||||
|
client.emit('project:access:revoked')
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
} else {
|
} else {
|
||||||
client.emit(message.message, ...message.payload)
|
if (
|
||||||
|
!(isRestrictedMessage && client.ol_context.is_restricted_user)
|
||||||
|
) {
|
||||||
|
client.emit(message.message, ...message.payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ describe('receiveEditorEvent', function () {
|
||||||
this.user_b_id = null
|
this.user_b_id = null
|
||||||
this.user_b_client = null
|
this.user_b_client = null
|
||||||
|
|
||||||
|
this.user_c_id = null
|
||||||
|
this.user_c_client = null
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
/**
|
/**
|
||||||
|
@ -51,6 +54,7 @@ describe('receiveEditorEvent', function () {
|
||||||
{
|
{
|
||||||
privilegeLevel: 'owner',
|
privilegeLevel: 'owner',
|
||||||
project: { name: 'Test Project' },
|
project: { name: 'Test Project' },
|
||||||
|
userMetadata: { isInvitedMember: true },
|
||||||
},
|
},
|
||||||
(error, { user_id: userId, project_id: projectId }) => {
|
(error, { user_id: userId, project_id: projectId }) => {
|
||||||
if (error) return done(error)
|
if (error) return done(error)
|
||||||
|
@ -95,13 +99,14 @@ describe('receiveEditorEvent', function () {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add user_a to project
|
* add user_a to project, as an invited member
|
||||||
*/
|
*/
|
||||||
cb => {
|
cb => {
|
||||||
return FixturesManager.setUpProject(
|
return FixturesManager.setUpProject(
|
||||||
{
|
{
|
||||||
privilegeLevel: 'readAndWrite',
|
privilegeLevel: 'readAndWrite',
|
||||||
project_id: this.project_id,
|
project_id: this.project_id,
|
||||||
|
userMetadata: { isTokenMember: false, isInvitedMember: true },
|
||||||
},
|
},
|
||||||
(error, { user_id: userIdSecond }) => {
|
(error, { user_id: userIdSecond }) => {
|
||||||
if (error) return done(error)
|
if (error) return done(error)
|
||||||
|
@ -133,13 +138,14 @@ describe('receiveEditorEvent', function () {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up user_b
|
* Set up user_b, as a token-access/link-sharing user
|
||||||
*/
|
*/
|
||||||
cb => {
|
cb => {
|
||||||
return FixturesManager.setUpProject(
|
return FixturesManager.setUpProject(
|
||||||
{
|
{
|
||||||
privilegeLevel: 'readAndWrite',
|
privilegeLevel: 'readAndWrite',
|
||||||
project_id: this.project_id,
|
project_id: this.project_id,
|
||||||
|
userMetadata: { isTokenMember: true, isInvitedMember: false },
|
||||||
},
|
},
|
||||||
(error, { user_id: userIdThird }) => {
|
(error, { user_id: userIdThird }) => {
|
||||||
if (error) return done(error)
|
if (error) return done(error)
|
||||||
|
@ -170,23 +176,81 @@ describe('receiveEditorEvent', function () {
|
||||||
return this.user_b_client.emit('joinDoc', this.doc_id, cb)
|
return this.user_b_client.emit('joinDoc', this.doc_id, cb)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up user_c, as a 'restricted' user (anonymous read-only link-sharing)
|
||||||
|
*/
|
||||||
|
cb => {
|
||||||
|
return FixturesManager.setUpProject(
|
||||||
|
{
|
||||||
|
privilegeLevel: 'readAndWrite',
|
||||||
|
project_id: this.project_id,
|
||||||
|
userMetadata: {
|
||||||
|
isTokenMember: false,
|
||||||
|
isInvitedMember: false,
|
||||||
|
isRestrictedUser: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(error, { user_id: userIdFourth }) => {
|
||||||
|
if (error) return done(error)
|
||||||
|
this.user_c_id = userIdFourth
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect user_c to project/doc
|
||||||
|
*/
|
||||||
|
cb => {
|
||||||
|
this.user_c_client = RealTimeClient.connect()
|
||||||
|
return this.user_c_client.on('connectionAccepted', cb)
|
||||||
|
},
|
||||||
|
|
||||||
|
cb => {
|
||||||
|
return this.user_c_client.emit(
|
||||||
|
'joinProject',
|
||||||
|
{
|
||||||
|
project_id: this.project_id,
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
return this.user_c_client.emit('joinDoc', this.doc_id, cb)
|
||||||
|
},
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for updates
|
* Listen for updates
|
||||||
*/
|
*/
|
||||||
cb => {
|
cb => {
|
||||||
const eventName = 'userRemovedFromProject'
|
|
||||||
this.owner_updates = []
|
this.owner_updates = []
|
||||||
this.owner_client.on(eventName, update =>
|
|
||||||
this.owner_updates.push({ [eventName]: update })
|
|
||||||
)
|
|
||||||
this.user_a_updates = []
|
this.user_a_updates = []
|
||||||
this.user_a_client.on(eventName, update =>
|
|
||||||
this.user_a_updates.push({ [eventName]: update })
|
|
||||||
)
|
|
||||||
this.user_b_updates = []
|
this.user_b_updates = []
|
||||||
this.user_b_client.on(eventName, update =>
|
this.user_c_updates = []
|
||||||
this.user_b_updates.push({ [eventName]: update })
|
|
||||||
)
|
const eventNames = [
|
||||||
|
'userRemovedFromProject',
|
||||||
|
'project:publicAccessLevel:changed',
|
||||||
|
'project:access:revoked',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const eventName of eventNames) {
|
||||||
|
this.owner_client.on(eventName, update =>
|
||||||
|
this.owner_updates.push({ [eventName]: update })
|
||||||
|
)
|
||||||
|
this.user_a_client.on(eventName, update =>
|
||||||
|
this.user_a_updates.push({ [eventName]: update })
|
||||||
|
)
|
||||||
|
this.user_b_client.on(eventName, update =>
|
||||||
|
this.user_b_updates.push({ [eventName]: update })
|
||||||
|
)
|
||||||
|
this.user_c_client.on(eventName, update =>
|
||||||
|
this.user_c_updates.push({ [eventName]: update })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return cb()
|
return cb()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -204,6 +268,101 @@ describe('receiveEditorEvent', function () {
|
||||||
if (this.user_b_client) {
|
if (this.user_b_client) {
|
||||||
this.user_b_client.disconnect()
|
this.user_b_client.disconnect()
|
||||||
}
|
}
|
||||||
|
if (this.user_c_client) {
|
||||||
|
this.user_c_client.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('event: project:publicAccessLevel:changed, set to private', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
/**
|
||||||
|
* We turn off link sharing
|
||||||
|
*/
|
||||||
|
rclient.publish(
|
||||||
|
'editor-events',
|
||||||
|
JSON.stringify({
|
||||||
|
room_id: this.project_id,
|
||||||
|
message: 'project:publicAccessLevel:changed',
|
||||||
|
payload: [{ newAccessLevel: 'private' }],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setTimeout(done, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disconnect the token-access user, and restricted users', function () {
|
||||||
|
expect(this.user_b_client.socket.connected).to.equal(false)
|
||||||
|
expect(this.user_c_client.socket.connected).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not disconnect the other users', function () {
|
||||||
|
expect(this.owner_client.socket.connected).to.equal(true)
|
||||||
|
expect(this.user_a_client.socket.connected).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send the event to the remaining connected clients', function () {
|
||||||
|
expect(this.owner_updates).to.deep.equal([
|
||||||
|
{ 'project:publicAccessLevel:changed': { newAccessLevel: 'private' } },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(this.user_a_updates).to.deep.equal([
|
||||||
|
{ 'project:publicAccessLevel:changed': { newAccessLevel: 'private' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send a project:access:revoked message to the disconnected clients', function () {
|
||||||
|
expect(this.user_b_updates).to.deep.equal([
|
||||||
|
{ 'project:access:revoked': undefined },
|
||||||
|
])
|
||||||
|
expect(this.user_c_updates).to.deep.equal([
|
||||||
|
{ 'project:access:revoked': undefined },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('event: project:publicAccessLevel:changed, set to tokenBased', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
/**
|
||||||
|
* We turn on link sharing
|
||||||
|
*/
|
||||||
|
rclient.publish(
|
||||||
|
'editor-events',
|
||||||
|
JSON.stringify({
|
||||||
|
room_id: this.project_id,
|
||||||
|
message: 'project:publicAccessLevel:changed',
|
||||||
|
payload: [{ newAccessLevel: 'tokenBased' }],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setTimeout(done, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not disconnect anyone', function () {
|
||||||
|
expect(this.owner_client.socket.connected).to.equal(true)
|
||||||
|
expect(this.user_a_client.socket.connected).to.equal(true)
|
||||||
|
expect(this.user_b_client.socket.connected).to.equal(true)
|
||||||
|
expect(this.user_c_client.socket.connected).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send the event to all non-restricted clients', function () {
|
||||||
|
expect(this.owner_updates).to.deep.equal([
|
||||||
|
{
|
||||||
|
'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(this.user_a_updates).to.deep.equal([
|
||||||
|
{
|
||||||
|
'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(this.user_b_updates).to.deep.equal([
|
||||||
|
{
|
||||||
|
'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
// restricted users don't receive this type of message
|
||||||
|
expect(this.user_c_updates.length).to.equal(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('event: userRemovedFromProject', function () {
|
describe('event: userRemovedFromProject', function () {
|
||||||
|
@ -238,11 +397,15 @@ describe('receiveEditorEvent', function () {
|
||||||
{ userRemovedFromProject: removedUserId },
|
{ userRemovedFromProject: removedUserId },
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(this.user_a_updates.length).to.equal(0)
|
|
||||||
|
|
||||||
expect(this.user_b_updates).to.deep.equal([
|
expect(this.user_b_updates).to.deep.equal([
|
||||||
{ userRemovedFromProject: removedUserId },
|
{ userRemovedFromProject: removedUserId },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should send a project:access:revoked message to the disconnected clients', function () {
|
||||||
|
expect(this.user_a_updates).to.deep.equal([
|
||||||
|
{ 'project:access:revoked': undefined },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,6 +34,7 @@ module.exports = FixturesManager = {
|
||||||
privilegeLevel,
|
privilegeLevel,
|
||||||
project,
|
project,
|
||||||
publicAccess,
|
publicAccess,
|
||||||
|
userMetadata,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const privileges = {}
|
const privileges = {}
|
||||||
|
@ -42,7 +43,15 @@ module.exports = FixturesManager = {
|
||||||
privileges['anonymous-user'] = publicAccess
|
privileges['anonymous-user'] = publicAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
MockWebServer.createMockProject(projectId, privileges, project)
|
const metadataByUser = {}
|
||||||
|
metadataByUser[userId] = userMetadata
|
||||||
|
|
||||||
|
MockWebServer.createMockProject(
|
||||||
|
projectId,
|
||||||
|
privileges,
|
||||||
|
project,
|
||||||
|
metadataByUser
|
||||||
|
)
|
||||||
return MockWebServer.run(error => {
|
return MockWebServer.run(error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -16,9 +16,11 @@ const express = require('express')
|
||||||
module.exports = MockWebServer = {
|
module.exports = MockWebServer = {
|
||||||
projects: {},
|
projects: {},
|
||||||
privileges: {},
|
privileges: {},
|
||||||
|
userMetadata: {},
|
||||||
|
|
||||||
createMockProject(projectId, privileges, project) {
|
createMockProject(projectId, privileges, project, metadataByUser) {
|
||||||
MockWebServer.privileges[projectId] = privileges
|
MockWebServer.privileges[projectId] = privileges
|
||||||
|
MockWebServer.userMetadata[projectId] = metadataByUser
|
||||||
return (MockWebServer.projects[projectId] = project)
|
return (MockWebServer.projects[projectId] = project)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -26,12 +28,12 @@ module.exports = MockWebServer = {
|
||||||
if (callback == null) {
|
if (callback == null) {
|
||||||
callback = function () {}
|
callback = function () {}
|
||||||
}
|
}
|
||||||
return callback(
|
const project = MockWebServer.projects[projectId]
|
||||||
null,
|
const privilegeLevel =
|
||||||
MockWebServer.projects[projectId],
|
|
||||||
MockWebServer.privileges[projectId][userId] ||
|
MockWebServer.privileges[projectId][userId] ||
|
||||||
MockWebServer.privileges[projectId]['anonymous-user']
|
MockWebServer.privileges[projectId]['anonymous-user']
|
||||||
)
|
const userMetadata = MockWebServer.userMetadata[projectId]?.[userId]
|
||||||
|
return callback(null, project, privilegeLevel, userMetadata)
|
||||||
},
|
},
|
||||||
|
|
||||||
joinProjectRequest(req, res, next) {
|
joinProjectRequest(req, res, next) {
|
||||||
|
@ -52,13 +54,16 @@ module.exports = MockWebServer = {
|
||||||
return MockWebServer.joinProject(
|
return MockWebServer.joinProject(
|
||||||
projectId,
|
projectId,
|
||||||
userId,
|
userId,
|
||||||
(error, project, privilegeLevel) => {
|
(error, project, privilegeLevel, userMetadata) => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
return res.json({
|
return res.json({
|
||||||
project,
|
project,
|
||||||
privilegeLevel,
|
privilegeLevel,
|
||||||
|
isRestrictedUser: !!userMetadata?.isRestrictedUser,
|
||||||
|
isTokenMember: !!userMetadata?.isTokenMember,
|
||||||
|
isInvitedMember: !!userMetadata?.isInvitedMember,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,6 +43,8 @@ describe('WebApiManager', function () {
|
||||||
project: { name: 'Test project' },
|
project: { name: 'Test project' },
|
||||||
privilegeLevel: 'owner',
|
privilegeLevel: 'owner',
|
||||||
isRestrictedUser: true,
|
isRestrictedUser: true,
|
||||||
|
isTokenMember: true,
|
||||||
|
isInvitedMember: true,
|
||||||
}
|
}
|
||||||
this.request.post = sinon
|
this.request.post = sinon
|
||||||
.stub()
|
.stub()
|
||||||
|
@ -79,7 +81,11 @@ describe('WebApiManager', function () {
|
||||||
null,
|
null,
|
||||||
this.response.project,
|
this.response.project,
|
||||||
this.response.privilegeLevel,
|
this.response.privilegeLevel,
|
||||||
this.response.isRestrictedUser
|
{
|
||||||
|
isRestrictedUser: this.response.isRestrictedUser,
|
||||||
|
isTokenMember: this.response.isTokenMember,
|
||||||
|
isInvitedMember: this.response.isInvitedMember,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
})
|
})
|
||||||
|
|
|
@ -75,15 +75,15 @@ describe('WebsocketController', function () {
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgAsync(4)
|
.callsArgAsync(4)
|
||||||
this.isRestrictedUser = true
|
this.isRestrictedUser = true
|
||||||
|
this.isTokenMember = true
|
||||||
|
this.isInvitedMember = true
|
||||||
this.WebApiManager.joinProject = sinon
|
this.WebApiManager.joinProject = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(
|
.callsArgWith(2, null, this.project, this.privilegeLevel, {
|
||||||
2,
|
isRestrictedUser: this.isRestrictedUser,
|
||||||
null,
|
isTokenMember: this.isTokenMember,
|
||||||
this.project,
|
isInvitedMember: this.isInvitedMember,
|
||||||
this.privilegeLevel,
|
})
|
||||||
this.isRestrictedUser
|
|
||||||
)
|
|
||||||
this.RoomManager.joinProject = sinon.stub().callsArg(2)
|
this.RoomManager.joinProject = sinon.stub().callsArg(2)
|
||||||
return this.WebsocketController.joinProject(
|
return this.WebsocketController.joinProject(
|
||||||
this.client,
|
this.client,
|
||||||
|
@ -150,6 +150,14 @@ describe('WebsocketController', function () {
|
||||||
this.isRestrictedUser
|
this.isRestrictedUser
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it('should set the is_token_member flag on the client', function () {
|
||||||
|
this.client.ol_context.is_token_member.should.equal(this.isTokenMember)
|
||||||
|
})
|
||||||
|
it('should set the is_invited_member flag on the client', function () {
|
||||||
|
this.client.ol_context.is_invited_member.should.equal(
|
||||||
|
this.isInvitedMember
|
||||||
|
)
|
||||||
|
})
|
||||||
it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
|
it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
|
||||||
return this.callback
|
return this.callback
|
||||||
.calledWith(
|
.calledWith(
|
||||||
|
@ -212,15 +220,15 @@ describe('WebsocketController', function () {
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgAsync(4)
|
.callsArgAsync(4)
|
||||||
this.isRestrictedUser = true
|
this.isRestrictedUser = true
|
||||||
|
this.isTokenMember = true
|
||||||
|
this.isInvitedMember = true
|
||||||
this.WebApiManager.joinProject = sinon
|
this.WebApiManager.joinProject = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(
|
.callsArgWith(2, null, this.project, this.privilegeLevel, {
|
||||||
2,
|
isRestrictedUser: this.isRestrictedUser,
|
||||||
null,
|
isTokenMember: this.isTokenMember,
|
||||||
this.project,
|
isInvitedMember: this.isInvitedMember,
|
||||||
this.privilegeLevel,
|
})
|
||||||
this.isRestrictedUser
|
|
||||||
)
|
|
||||||
this.RoomManager.joinProject = sinon
|
this.RoomManager.joinProject = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(2, new Error('subscribe failed'))
|
.callsArgWith(2, new Error('subscribe failed'))
|
||||||
|
@ -273,12 +281,11 @@ describe('WebsocketController', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.WebApiManager.joinProject = (project, user, cb) => {
|
this.WebApiManager.joinProject = (project, user, cb) => {
|
||||||
this.client.disconnected = true
|
this.client.disconnected = true
|
||||||
return cb(
|
return cb(null, this.project, this.privilegeLevel, {
|
||||||
null,
|
isRestrictedUser: this.isRestrictedUser,
|
||||||
this.project,
|
isTokenMember: this.isTokenMember,
|
||||||
this.privilegeLevel,
|
isInvitedMember: this.isInvitedMember,
|
||||||
this.isRestrictedUser
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.WebsocketController.joinProject(
|
return this.WebsocketController.joinProject(
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe('WebsocketLoadBalancer', function () {
|
||||||
this.RoomEvents = { on: sinon.stub() }
|
this.RoomEvents = { on: sinon.stub() }
|
||||||
this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, {
|
this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
|
'@overleaf/settings': (this.Settings = { redis: {} }),
|
||||||
'./RedisClientManager': {
|
'./RedisClientManager': {
|
||||||
createClientList: () => [],
|
createClientList: () => [],
|
||||||
},
|
},
|
||||||
|
@ -55,11 +56,10 @@ describe('WebsocketLoadBalancer', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('shouldDisconnectClient', function () {
|
describe('shouldDisconnectClient', function () {
|
||||||
const client = {
|
|
||||||
ol_context: { user_id: 'abcd' },
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should return false for general messages', function () {
|
it('should return false for general messages', function () {
|
||||||
|
const client = {
|
||||||
|
ol_context: { user_id: 'abcd' },
|
||||||
|
}
|
||||||
const message = {
|
const message = {
|
||||||
message: 'someNiceMessage',
|
message: 'someNiceMessage',
|
||||||
payload: [{ data: 'whatever' }],
|
payload: [{ data: 'whatever' }],
|
||||||
|
@ -69,24 +69,103 @@ describe('WebsocketLoadBalancer', function () {
|
||||||
).to.equal(false)
|
).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return false for userRemovedFromProject, when the user_id does not match', function () {
|
describe('user removed from project', function () {
|
||||||
const message = {
|
const messageName = 'userRemovedFromProject'
|
||||||
message: 'userRemovedFromProject',
|
const client = {
|
||||||
payload: ['xyz'],
|
ol_context: { user_id: 'abcd' },
|
||||||
}
|
}
|
||||||
expect(
|
it('should return false, when the user_id does not match', function () {
|
||||||
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
const message = {
|
||||||
).to.equal(false)
|
message: messageName,
|
||||||
|
payload: ['xyz'],
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true, if the user_id matches', function () {
|
||||||
|
const message = {
|
||||||
|
message: messageName,
|
||||||
|
payload: [`${client.ol_context.user_id}`],
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return true for userRemovedFromProject, if the user_id matches', function () {
|
describe('link-sharing turned off', function () {
|
||||||
const message = {
|
const messageName = 'project:publicAccessLevel:changed'
|
||||||
message: 'userRemovedFromProject',
|
|
||||||
payload: [`${client.ol_context.user_id}`],
|
describe('when the new access level is set to "private"', function () {
|
||||||
}
|
const message = {
|
||||||
expect(
|
message: messageName,
|
||||||
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
payload: [{ newAccessLevel: 'private' }],
|
||||||
).to.equal(true)
|
}
|
||||||
|
describe('when the user is an invited member', function () {
|
||||||
|
const client = {
|
||||||
|
ol_context: {
|
||||||
|
is_invited_member: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return false', function () {
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the user not an invited member', function () {
|
||||||
|
const client = {
|
||||||
|
ol_context: {
|
||||||
|
is_invited_member: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return true', function () {
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the new access level is "tokenBased"', function () {
|
||||||
|
const message = {
|
||||||
|
message: messageName,
|
||||||
|
payload: [{ newAccessLevel: 'tokenBased' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('when the user is an invited member', function () {
|
||||||
|
const client = {
|
||||||
|
ol_context: {
|
||||||
|
is_invited_member: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return false', function () {
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the user not an invited member', function () {
|
||||||
|
const client = {
|
||||||
|
ol_context: {
|
||||||
|
is_invited_member: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return false', function () {
|
||||||
|
expect(
|
||||||
|
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
|
||||||
|
).to.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -392,7 +471,7 @@ describe('WebsocketLoadBalancer', function () {
|
||||||
client1.emit
|
client1.emit
|
||||||
.calledWith(message, ...Array.from(payload))
|
.calledWith(message, ...Array.from(payload))
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
client2.emit.called.should.equal(false) // disconnected client should not be called
|
client2.emit.calledWith('project:access:revoked').should.equal(true) // disconnected client should get informative message
|
||||||
client3.emit
|
client3.emit
|
||||||
.calledWith(message, ...Array.from(payload))
|
.calledWith(message, ...Array.from(payload))
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
|
|
|
@ -57,8 +57,13 @@ async function joinProject(req, res, next) {
|
||||||
userId = null
|
userId = null
|
||||||
}
|
}
|
||||||
Metrics.inc('editor.join-project')
|
Metrics.inc('editor.join-project')
|
||||||
const { project, privilegeLevel, isRestrictedUser } =
|
const {
|
||||||
await _buildJoinProjectView(req, projectId, userId)
|
project,
|
||||||
|
privilegeLevel,
|
||||||
|
isRestrictedUser,
|
||||||
|
isTokenMember,
|
||||||
|
isInvitedMember,
|
||||||
|
} = await _buildJoinProjectView(req, projectId, userId)
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +90,8 @@ async function joinProject(req, res, next) {
|
||||||
project,
|
project,
|
||||||
privilegeLevel,
|
privilegeLevel,
|
||||||
isRestrictedUser,
|
isRestrictedUser,
|
||||||
|
isTokenMember,
|
||||||
|
isInvitedMember,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +157,8 @@ async function _buildJoinProjectView(req, projectId, userId) {
|
||||||
deletedDocsFromDocstore
|
deletedDocsFromDocstore
|
||||||
),
|
),
|
||||||
privilegeLevel,
|
privilegeLevel,
|
||||||
|
isTokenMember,
|
||||||
|
isInvitedMember,
|
||||||
isRestrictedUser,
|
isRestrictedUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -476,6 +476,13 @@ If the project has been renamed please look in your project list for a new proje
|
||||||
$scope.hasLintingError = event.detail.hasLintingError
|
$scope.hasLintingError = event.detail.hasLintingError
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ide.socket.on('project:access:revoked', () => {
|
||||||
|
ide.showGenericMessageModal(
|
||||||
|
'Removed From Project',
|
||||||
|
'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return ide.socket.on('project:publicAccessLevel:changed', data => {
|
return ide.socket.on('project:publicAccessLevel:changed', data => {
|
||||||
if (data.newAccessLevel != null) {
|
if (data.newAccessLevel != null) {
|
||||||
ide.$scope.project.publicAccesLevel = data.newAccessLevel
|
ide.$scope.project.publicAccesLevel = data.newAccessLevel
|
||||||
|
|
|
@ -380,6 +380,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readOnly')
|
expect(body.privilegeLevel).to.equal('readOnly')
|
||||||
expect(body.isRestrictedUser).to.equal(true)
|
expect(body.isRestrictedUser).to.equal(true)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
expect(body.project.owner).to.have.keys('_id')
|
expect(body.project.owner).to.have.keys('_id')
|
||||||
expect(body.project.owner).to.not.have.any.keys(
|
expect(body.project.owner).to.not.have.any.keys(
|
||||||
'email',
|
'email',
|
||||||
|
@ -431,6 +433,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('owner')
|
expect(body.privilegeLevel).to.equal('owner')
|
||||||
expect(body.isRestrictedUser).to.equal(false)
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(false)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
},
|
},
|
||||||
cb
|
cb
|
||||||
)
|
)
|
||||||
|
@ -560,6 +564,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readOnly')
|
expect(body.privilegeLevel).to.equal('readOnly')
|
||||||
expect(body.isRestrictedUser).to.equal(true)
|
expect(body.isRestrictedUser).to.equal(true)
|
||||||
|
expect(body.isTokenMember).to.equal(false)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
expect(body.project.owner).to.have.keys('_id')
|
expect(body.project.owner).to.have.keys('_id')
|
||||||
expect(body.project.owner).to.not.have.any.keys(
|
expect(body.project.owner).to.not.have.any.keys(
|
||||||
'email',
|
'email',
|
||||||
|
@ -718,6 +724,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readAndWrite')
|
expect(body.privilegeLevel).to.equal('readAndWrite')
|
||||||
expect(body.isRestrictedUser).to.equal(false)
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
expect(body.project.owner).to.have.all.keys(
|
expect(body.project.owner).to.have.all.keys(
|
||||||
'_id',
|
'_id',
|
||||||
'email',
|
'email',
|
||||||
|
@ -804,6 +812,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readOnly')
|
expect(body.privilegeLevel).to.equal('readOnly')
|
||||||
expect(body.isRestrictedUser).to.equal(true)
|
expect(body.isRestrictedUser).to.equal(true)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
expect(body.project.owner).to.have.keys('_id')
|
expect(body.project.owner).to.have.keys('_id')
|
||||||
expect(body.project.owner).to.not.have.any.keys(
|
expect(body.project.owner).to.not.have.any.keys(
|
||||||
'email',
|
'email',
|
||||||
|
@ -847,6 +857,8 @@ describe('TokenAccess', function () {
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readAndWrite')
|
expect(body.privilegeLevel).to.equal('readAndWrite')
|
||||||
expect(body.isRestrictedUser).to.equal(false)
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(true)
|
||||||
|
expect(body.isInvitedMember).to.equal(false)
|
||||||
expect(body.project.owner).to.have.all.keys(
|
expect(body.project.owner).to.have.all.keys(
|
||||||
'_id',
|
'_id',
|
||||||
'email',
|
'email',
|
||||||
|
@ -1352,6 +1364,9 @@ describe('TokenAccess', function () {
|
||||||
this.projectId,
|
this.projectId,
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
expect(body.privilegeLevel).to.equal('readAndWrite')
|
expect(body.privilegeLevel).to.equal('readAndWrite')
|
||||||
|
expect(body.isRestrictedUser).to.equal(false)
|
||||||
|
expect(body.isTokenMember).to.equal(false)
|
||||||
|
expect(body.isInvitedMember).to.equal(true)
|
||||||
},
|
},
|
||||||
cb
|
cb
|
||||||
),
|
),
|
||||||
|
|
|
@ -161,6 +161,9 @@ describe('EditorHttpController', function () {
|
||||||
|
|
||||||
describe('successfully', function () {
|
describe('successfully', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
|
||||||
|
true
|
||||||
|
)
|
||||||
this.res.json.callsFake(() => done())
|
this.res.json.callsFake(() => done())
|
||||||
this.EditorHttpController.joinProject(this.req, this.res)
|
this.EditorHttpController.joinProject(this.req, this.res)
|
||||||
})
|
})
|
||||||
|
@ -170,6 +173,8 @@ describe('EditorHttpController', function () {
|
||||||
project: this.projectView,
|
project: this.projectView,
|
||||||
privilegeLevel: 'owner',
|
privilegeLevel: 'owner',
|
||||||
isRestrictedUser: false,
|
isRestrictedUser: false,
|
||||||
|
isTokenMember: false,
|
||||||
|
isInvitedMember: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -212,6 +217,8 @@ describe('EditorHttpController', function () {
|
||||||
project: this.reducedProjectView,
|
project: this.reducedProjectView,
|
||||||
privilegeLevel: 'readOnly',
|
privilegeLevel: 'readOnly',
|
||||||
isRestrictedUser: true,
|
isRestrictedUser: true,
|
||||||
|
isTokenMember: false,
|
||||||
|
isInvitedMember: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -248,6 +255,32 @@ describe('EditorHttpController', function () {
|
||||||
project: this.reducedProjectView,
|
project: this.reducedProjectView,
|
||||||
privilegeLevel: 'readOnly',
|
privilegeLevel: 'readOnly',
|
||||||
isRestrictedUser: true,
|
isRestrictedUser: true,
|
||||||
|
isTokenMember: false,
|
||||||
|
isInvitedMember: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a token access user', function () {
|
||||||
|
beforeEach(function (done) {
|
||||||
|
this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
this.CollaboratorsHandler.promises.userIsTokenMember.resolves(true)
|
||||||
|
this.AuthorizationManager.promises.getPrivilegeLevelForProject.resolves(
|
||||||
|
'readAndWrite'
|
||||||
|
)
|
||||||
|
this.res.json.callsFake(() => done())
|
||||||
|
this.EditorHttpController.joinProject(this.req, this.res)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should mark the user as being a token-access member', function () {
|
||||||
|
expect(this.res.json).to.have.been.calledWith({
|
||||||
|
project: this.projectView,
|
||||||
|
privilegeLevel: 'readAndWrite',
|
||||||
|
isRestrictedUser: false,
|
||||||
|
isTokenMember: true,
|
||||||
|
isInvitedMember: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue