mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-01 05:11:34 +02:00
* add option to remove link-sharing from backend * restrict make link-sharing in the frontend based on capability * extend e2e project-sharing tests to cover OVERLEAF_DISABLE_LINK_SHARING=true * throw an error when link sharing is disabled in TokenAccessHandler * throw errors when attempting to add users to projects with link sharing disabled * Update server-ce/test/project-sharing.spec.ts Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com> * add tests for existing access when link sharing is disabled * update tests to specify access restrictions for read-only and read-write link shared projects * [web] block access to legacy public project with link-sharing disabled --------- Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com> GitOrigin-RevId: 5f194dbcb790e973e427c58a3a4a738a5dd74cb4
1558 lines
44 KiB
JavaScript
1558 lines
44 KiB
JavaScript
import { expect } from 'chai'
|
|
import async from 'async'
|
|
import User from './helpers/User.mjs'
|
|
import request from './helpers/request.js'
|
|
import settings from '@overleaf/settings'
|
|
import { db } from '../../../app/src/infrastructure/mongodb.js'
|
|
import expectErrorResponse from './helpers/expectErrorResponse.mjs'
|
|
import logger from '@overleaf/logger'
|
|
import sinon from 'sinon'
|
|
|
|
const tryEditorAccess = (user, projectId, test, callback) =>
|
|
async.series(
|
|
[
|
|
cb =>
|
|
user.request.get(`/project/${projectId}`, (error, response, body) => {
|
|
if (error != null) {
|
|
return cb(error)
|
|
}
|
|
test(response, body)
|
|
cb()
|
|
}),
|
|
cb =>
|
|
user.request.get(
|
|
`/project/${projectId}/download/zip`,
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return cb(error)
|
|
}
|
|
test(response, body)
|
|
cb()
|
|
}
|
|
),
|
|
],
|
|
callback
|
|
)
|
|
|
|
const tryReadOnlyTokenAccess = (
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
) => {
|
|
_doTryTokenAccess(
|
|
`/read/${token}`,
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
)
|
|
}
|
|
|
|
const tryReadAndWriteTokenAccess = (
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
) => {
|
|
_doTryTokenAccess(
|
|
`/${token}`,
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
)
|
|
}
|
|
|
|
const _doTryTokenAccess = (
|
|
url,
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
) => {
|
|
user.request.get(url, (err, response, body) => {
|
|
if (err) {
|
|
return callback(err)
|
|
}
|
|
testPageLoad(response, body)
|
|
if (!testFormPost) {
|
|
return callback()
|
|
}
|
|
user.request.post(
|
|
`${url}/grant`,
|
|
{ json: { token } },
|
|
(err, response, body) => {
|
|
if (err) {
|
|
return callback(err)
|
|
}
|
|
testFormPost(response, body)
|
|
callback()
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
const tryReadOnlyTokenAccept = (
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
) => {
|
|
_doTryTokenAccept(
|
|
`/read/${token}`,
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
)
|
|
}
|
|
|
|
const _doTryTokenAccept = (
|
|
url,
|
|
user,
|
|
token,
|
|
testPageLoad,
|
|
testFormPost,
|
|
callback
|
|
) => {
|
|
user.request.get(url, (err, response, body) => {
|
|
if (err) {
|
|
return callback(err)
|
|
}
|
|
testPageLoad(response, body)
|
|
if (!testFormPost) {
|
|
return callback()
|
|
}
|
|
user.request.post(
|
|
`${url}/grant`,
|
|
{ json: { token, confirmedByUser: true } },
|
|
(err, response, body) => {
|
|
if (err) {
|
|
return callback(err)
|
|
}
|
|
testFormPost(response, body)
|
|
callback()
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
const tryContentAccess = (user, projcetId, test, callback) => {
|
|
// The real-time service calls this end point to determine the user's
|
|
// permissions.
|
|
let userId
|
|
if (user.id != null) {
|
|
userId = user.id
|
|
} else {
|
|
userId = 'anonymous-user'
|
|
}
|
|
request.post(
|
|
{
|
|
url: `/project/${projcetId}/join`,
|
|
auth: {
|
|
user: settings.apis.web.user,
|
|
pass: settings.apis.web.pass,
|
|
sendImmediately: true,
|
|
},
|
|
json: {
|
|
userId,
|
|
},
|
|
jar: false,
|
|
},
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
test(response, body)
|
|
callback()
|
|
}
|
|
)
|
|
}
|
|
|
|
const tryAnonContentAccess = (user, projectId, token, test, callback) => {
|
|
// The real-time service calls this end point to determine the user's
|
|
// permissions.
|
|
let userId
|
|
if (user.id != null) {
|
|
userId = user.id
|
|
} else {
|
|
userId = 'anonymous-user'
|
|
}
|
|
request.post(
|
|
{
|
|
url: `/project/${projectId}/join`,
|
|
auth: {
|
|
user: settings.apis.web.user,
|
|
pass: settings.apis.web.pass,
|
|
sendImmediately: true,
|
|
},
|
|
json: {
|
|
userId,
|
|
anonymousAccessToken: token,
|
|
},
|
|
jar: false,
|
|
},
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
test(response, body)
|
|
callback()
|
|
}
|
|
)
|
|
}
|
|
|
|
const tryFetchProjectTokens = (user, projectId, callback) => {
|
|
user.request.get(
|
|
{ url: `/project/${projectId}/tokens`, json: true },
|
|
(error, response, body) => {
|
|
if (error) {
|
|
return callback(error)
|
|
}
|
|
callback(null, response, body)
|
|
}
|
|
)
|
|
}
|
|
|
|
const trySharingUpdatesPage = (user, projectId, test, callback) =>
|
|
user.request.get(
|
|
`/project/${projectId}/sharing-updates`,
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
test(response, body, projectId)
|
|
callback()
|
|
}
|
|
)
|
|
|
|
const trySharingUpdatesJoin = (user, projectId, test, callback) =>
|
|
user.request.post(
|
|
`/project/${projectId}/sharing-updates/join`,
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
test(response, body, projectId)
|
|
callback()
|
|
}
|
|
)
|
|
|
|
const trySharingUpdatesView = (user, projectId, test, callback) =>
|
|
user.request.post(
|
|
`/project/${projectId}/sharing-updates/view`,
|
|
(error, response, body) => {
|
|
if (error != null) {
|
|
return callback(error)
|
|
}
|
|
test(response, body, projectId)
|
|
callback()
|
|
}
|
|
)
|
|
|
|
const expectRedirectToProject = (response, body, projectId) => {
|
|
expect(response.statusCode).to.equal(302)
|
|
expect(response.headers.location).to.equal(`/project/${projectId}`)
|
|
}
|
|
|
|
describe('TokenAccess', function () {
|
|
beforeEach(function (done) {
|
|
this.timeout(90000)
|
|
this.owner = new User()
|
|
this.other1 = new User()
|
|
this.other2 = new User()
|
|
this.anon = new User()
|
|
this.siteAdmin = new User({ email: 'admin@example.com' })
|
|
async.parallel(
|
|
[
|
|
cb =>
|
|
this.siteAdmin.login(err => {
|
|
if (err) {
|
|
return cb(err)
|
|
}
|
|
this.siteAdmin.ensureAdmin(cb)
|
|
}),
|
|
cb => this.owner.login(cb),
|
|
cb => this.other1.login(cb),
|
|
cb => this.other2.login(cb),
|
|
cb => this.anon.getCsrfToken(cb),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
describe('no token-access', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject(
|
|
`token-ro-test${Math.random()}`,
|
|
(err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
// Note, never made token-based,
|
|
// thus no tokens
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should deny access ', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
tryContentAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should deny access to access tokens', function (done) {
|
|
tryFetchProjectTokens(this.other1, this.projectId, (error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(403)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('owner', function () {
|
|
beforeEach(function (done) {
|
|
this.projectName = `token-owner-test${Math.random()}`
|
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should be able to fetch read-only and read-write tokens', function (done) {
|
|
tryFetchProjectTokens(
|
|
this.owner,
|
|
this.projectId,
|
|
(error, response, body) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.include({
|
|
readOnly: this.tokens.readOnly,
|
|
readAndWrite: this.tokens.readAndWrite,
|
|
readAndWritePrefix: this.tokens.readAndWritePrefix,
|
|
})
|
|
expect(body.readOnlyHashPrefix).to.exist
|
|
expect(body.readAndWriteHashPrefix).to.exist
|
|
expect(Object.keys(body).length).to.equal(5)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('read-only token', function () {
|
|
beforeEach(function (done) {
|
|
this.projectName = `token-ro-test${Math.random()}`
|
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('allow the user read-only access to the project', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
// deny access before token is used
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// try token
|
|
tryReadOnlyTokenAccess(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.requireAccept.projectName).to.equal(
|
|
this.projectName
|
|
)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// deny access before token is accepted
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// accept token
|
|
tryReadOnlyTokenAccept(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
expect(body.tokenAccessGranted).to.equal('readOnly')
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// allow content access read-only
|
|
tryContentAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(body.privilegeLevel).to.equal('readOnly')
|
|
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.not.have.any.keys(
|
|
'email',
|
|
'first_name',
|
|
'last_name'
|
|
)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should allow the user to fetch the read-only token', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
// accept token
|
|
tryReadOnlyTokenAccept(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
expect(body.tokenAccessGranted).to.equal('readOnly')
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
tryFetchProjectTokens(
|
|
this.other1,
|
|
this.projectId,
|
|
(error, response, body) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.include({ readOnly: this.tokens.readOnly })
|
|
expect(body.readOnlyHashPrefix).to.exist
|
|
cb()
|
|
}
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should redirect the admin to the project (with rw access)', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
// use token
|
|
tryReadOnlyTokenAccess(
|
|
this.siteAdmin,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// allow content access read-and-write
|
|
tryContentAccess(
|
|
this.siteAdmin,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(body.privilegeLevel).to.equal('owner')
|
|
expect(body.isRestrictedUser).to.equal(false)
|
|
expect(body.isTokenMember).to.equal(false)
|
|
expect(body.isInvitedMember).to.equal(false)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
describe('made private again', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000))
|
|
})
|
|
|
|
it('should not allow the user to access the project', function (done) {
|
|
async.series(
|
|
[
|
|
// no access before token is used
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
// token goes nowhere
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(404)
|
|
},
|
|
cb
|
|
),
|
|
// still no access
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
cb =>
|
|
tryContentAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should deny access to access tokens', function (done) {
|
|
tryFetchProjectTokens(this.other1, this.projectId, (error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(403)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('anonymous read-only token', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject(
|
|
`token-anon-ro-test${Math.random()}`,
|
|
(err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should allow the user to access project via read-only token url', function (done) {
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.anon,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
expect(body.grantAnonymousAccess).to.equal('readOnly')
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(body.privilegeLevel).to.equal('readOnly')
|
|
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.not.have.any.keys(
|
|
'email',
|
|
'first_name',
|
|
'last_name'
|
|
)
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should allow the anonymous user to fetch the read-only token', function (done) {
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.anon,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
expect(body.grantAnonymousAccess).to.equal('readOnly')
|
|
},
|
|
cb
|
|
),
|
|
cb => {
|
|
tryFetchProjectTokens(
|
|
this.anon,
|
|
this.projectId,
|
|
(error, response, body) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.include({ readOnly: this.tokens.readOnly })
|
|
expect(body.readOnlyHashPrefix).to.exist
|
|
expect(Object.keys(body).length).to.equal(2)
|
|
cb()
|
|
}
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
describe('made private again', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000))
|
|
})
|
|
|
|
it('should deny access to project', function (done) {
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
// should not allow the user to access read-only token
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.anon,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(404)
|
|
},
|
|
cb
|
|
),
|
|
// still no access
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
// should not allow the user to join the project
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should deny access to access tokens', function (done) {
|
|
tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(403)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('link sharing disabled', function () {
|
|
const previous = settings.disableLinkSharing
|
|
let loggerStub
|
|
beforeEach(function () {
|
|
settings.disableLinkSharing = true
|
|
loggerStub = sinon.spy(logger, 'error')
|
|
})
|
|
afterEach(function () {
|
|
settings.disableLinkSharing = previous
|
|
loggerStub.restore()
|
|
})
|
|
|
|
it('should deny access to project', function (done) {
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
// should not allow the user to access read-only token
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.anon,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
// NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
// NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
|
|
expect(response.statusCode).to.equal(500)
|
|
expect(loggerStub).to.have.been.calledWithMatch(
|
|
{
|
|
err: { message: 'link sharing is disabled' },
|
|
},
|
|
'%s %s',
|
|
'POST',
|
|
`/read/${this.tokens.readOnly}/grant`
|
|
)
|
|
},
|
|
cb
|
|
),
|
|
// still no access
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
// should not allow the user to join the project
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should deny access to access tokens', function (done) {
|
|
tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(403)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('should deny access to legacy public project', function (done) {
|
|
async.series(
|
|
[
|
|
cb => this.owner.makePublic(this.projectId, 'readOnly', cb),
|
|
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('anonymous read-and-write token, disabled (feature is deprecated)', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject(
|
|
`token-anon-rw-test${Math.random()}`,
|
|
(err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not allow the user to access read-and-write token', function (done) {
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
cb =>
|
|
tryReadAndWriteTokenAccess(
|
|
this.anon,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.deep.equal({
|
|
redirect: '/restricted',
|
|
anonWriteAccessDenied: true,
|
|
})
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
this.anon.login((err, response, body) => {
|
|
expect(err).to.not.exist
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`)
|
|
cb()
|
|
}),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should deny access to access tokens', function (done) {
|
|
tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(403)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('should require login if project does not exist', function (done) {
|
|
async.series(
|
|
[
|
|
// delete project
|
|
cb => {
|
|
this.owner.deleteProject(this.projectId, cb)
|
|
},
|
|
cb =>
|
|
tryReadAndWriteTokenAccess(
|
|
this.anon,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.deep.equal({
|
|
redirect: '/restricted',
|
|
anonWriteAccessDenied: true,
|
|
})
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
this.anon.login((err, response, body) => {
|
|
expect(err).to.not.exist
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`)
|
|
cb()
|
|
}),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should save URL hash in redirect', function (done) {
|
|
const urlFragment = '#123456'
|
|
const tokenWithUrlFragment = `${this.tokens.readAndWrite}${urlFragment}`
|
|
|
|
async.series(
|
|
[
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
cb =>
|
|
this.anon.request.get(
|
|
tokenWithUrlFragment,
|
|
(err, response, body) => {
|
|
if (err) {
|
|
return cb(err)
|
|
}
|
|
expect(response.statusCode).to.equal(200)
|
|
|
|
this.anon.request.post(
|
|
`${this.tokens.readAndWrite}/grant`,
|
|
{
|
|
json: {
|
|
token: this.tokens.readAndWrite,
|
|
tokenHashPrefix: urlFragment,
|
|
},
|
|
},
|
|
(err, response, body) => {
|
|
if (err) {
|
|
return cb(err)
|
|
}
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body).to.deep.equal({
|
|
redirect: '/restricted',
|
|
anonWriteAccessDenied: true,
|
|
})
|
|
cb()
|
|
}
|
|
)
|
|
}
|
|
),
|
|
cb =>
|
|
tryAnonContentAccess(
|
|
this.anon,
|
|
this.projectId,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
this.anon.login((err, response, body) => {
|
|
expect(err).to.not.exist
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redir).to.equal(`/${tokenWithUrlFragment}`)
|
|
cb()
|
|
}),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('private overleaf project', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject('overleaf-import', (err, projectId) => {
|
|
expect(err).not.to.exist
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
expect(err).not.to.exist
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
expect(err).not.to.exist
|
|
this.tokens = project.tokens
|
|
this.owner.makePrivate(this.projectId, () => {
|
|
db.projects.updateOne(
|
|
{ _id: project._id },
|
|
{
|
|
$set: {
|
|
overleaf: { id: 1234 },
|
|
},
|
|
},
|
|
err => {
|
|
expect(err).not.to.exist
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should only allow the owner access to the project', function (done) {
|
|
async.series(
|
|
[
|
|
// should redirect to canonical path, when owner uses read-write token
|
|
cb =>
|
|
tryReadAndWriteTokenAccess(
|
|
this.owner,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(response.body.redirect).to.equal(
|
|
`/project/${this.projectId}`
|
|
)
|
|
expect(response.body.higherAccess).to.equal(true)
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.owner,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
cb
|
|
),
|
|
cb =>
|
|
tryContentAccess(
|
|
this.owner,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(body.privilegeLevel).to.equal('owner')
|
|
},
|
|
cb
|
|
),
|
|
// non-owner should be denied access
|
|
cb =>
|
|
tryContentAccess(
|
|
this.other2,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('private project, with higher access', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject(
|
|
`higher-access-test-${Math.random()}`,
|
|
(err, projectId) => {
|
|
expect(err).not.to.exist
|
|
this.projectId = projectId
|
|
this.owner.addUserToProject(
|
|
this.projectId,
|
|
this.other1,
|
|
'readAndWrite',
|
|
err => {
|
|
expect(err).not.to.exist
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
expect(err).not.to.exist
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
expect(err).not.to.exist
|
|
this.tokens = project.tokens
|
|
this.owner.makePrivate(this.projectId, () => {
|
|
setTimeout(done, 1000)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should allow the user access to the project', function (done) {
|
|
async.series(
|
|
[
|
|
// should redirect to canonical path, when user uses read-write token
|
|
cb =>
|
|
tryReadAndWriteTokenAccess(
|
|
this.other1,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(response.body.redirect).to.equal(
|
|
`/project/${this.projectId}`
|
|
)
|
|
expect(response.body.higherAccess).to.equal(true)
|
|
},
|
|
cb
|
|
),
|
|
// should redirect to canonical path, when user uses read-only token
|
|
cb =>
|
|
tryReadOnlyTokenAccess(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(response.body.redirect).to.equal(
|
|
`/project/${this.projectId}`
|
|
)
|
|
expect(response.body.higherAccess).to.equal(true)
|
|
},
|
|
cb
|
|
),
|
|
// should allow the user access to the project
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
cb
|
|
),
|
|
// should allow user to join the project
|
|
cb =>
|
|
tryContentAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
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
|
|
),
|
|
// should not allow a different user to join the project
|
|
cb =>
|
|
tryEditorAccess(
|
|
this.other2,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
),
|
|
cb =>
|
|
tryContentAccess(
|
|
this.other2,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(403)
|
|
expect(body).to.equal('Forbidden')
|
|
},
|
|
cb
|
|
),
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('deleted project', function () {
|
|
beforeEach(function (done) {
|
|
this.owner.createProject(
|
|
`delete-test${Math.random()}`,
|
|
(err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should 404', function (done) {
|
|
async.series(
|
|
[
|
|
// delete project
|
|
cb => {
|
|
this.owner.deleteProject(this.projectId, cb)
|
|
},
|
|
cb => {
|
|
// use read-only token
|
|
tryReadOnlyTokenAccess(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(404)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// use read-write token
|
|
tryReadAndWriteTokenAccess(
|
|
this.other1,
|
|
this.tokens.readAndWrite,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(404)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should deny access to access tokens with a 404', function (done) {
|
|
async.series(
|
|
[
|
|
// delete project
|
|
cb => {
|
|
this.owner.deleteProject(this.projectId, cb)
|
|
},
|
|
cb => {
|
|
// use read-only token
|
|
tryReadOnlyTokenAccess(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(404)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
tryFetchProjectTokens(
|
|
this.other1,
|
|
this.projectId,
|
|
(error, response) => {
|
|
expect(error).to.equal(null)
|
|
expect(response.statusCode).to.equal(404)
|
|
cb()
|
|
}
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('sharing updates consent page for read-and-write token deprecation', function () {
|
|
describe('not a member of the project', function () {
|
|
beforeEach(function (done) {
|
|
this.projectName = `token-link-sharing-changes${Math.random()}`
|
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should deny access', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
trySharingUpdatesPage(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
trySharingUpdatesJoin(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
trySharingUpdatesView(
|
|
this.other1,
|
|
this.projectId,
|
|
expectErrorResponse.restricted.html,
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('read-only token member of project', function () {
|
|
beforeEach(function (done) {
|
|
this.projectName = `token-link-sharing-changes${Math.random()}`
|
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.projectId = projectId
|
|
this.owner.makeTokenBased(this.projectId, err => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.owner.getProject(this.projectId, (err, project) => {
|
|
if (err != null) {
|
|
return done(err)
|
|
}
|
|
this.tokens = project.tokens
|
|
tryReadOnlyTokenAccept(
|
|
this.other1,
|
|
this.tokens.readOnly,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
expect(body.redirect).to.equal(`/project/${this.projectId}`)
|
|
expect(body.tokenAccessGranted).to.equal('readOnly')
|
|
},
|
|
done
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should redirect to project', function (done) {
|
|
async.series(
|
|
[
|
|
cb => {
|
|
trySharingUpdatesPage(
|
|
this.other1,
|
|
this.projectId,
|
|
expectRedirectToProject,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
trySharingUpdatesJoin(
|
|
this.other1,
|
|
this.projectId,
|
|
expectRedirectToProject,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
trySharingUpdatesView(
|
|
this.other1,
|
|
this.projectId,
|
|
expectRedirectToProject,
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
// allow content access read-only
|
|
tryContentAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(body.privilegeLevel).to.equal('readOnly')
|
|
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.not.have.any.keys(
|
|
'email',
|
|
'first_name',
|
|
'last_name'
|
|
)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
cb => {
|
|
tryEditorAccess(
|
|
this.other1,
|
|
this.projectId,
|
|
(response, body) => {
|
|
expect(response.statusCode).to.equal(200)
|
|
},
|
|
cb
|
|
)
|
|
},
|
|
],
|
|
done
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|