mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-20 12:02:20 +00:00
Merge pull request #17380 from overleaf/dp-mongoose-callback-toke-access-handler
Promisify TokenAccessHandler and TokenAccessHandlerTests GitOrigin-RevId: 835081e78977456a59b7e16043fd6dcbdbce3ade
This commit is contained in:
parent
887a404fdd
commit
43007539a0
2 changed files with 392 additions and 476 deletions
|
@ -7,7 +7,7 @@ const Settings = require('@overleaf/settings')
|
|||
const logger = require('@overleaf/logger')
|
||||
const V1Api = require('../V1/V1Api')
|
||||
const crypto = require('crypto')
|
||||
const { promisifyAll } = require('@overleaf/promise-utils')
|
||||
const { callbackifyAll } = require('@overleaf/promise-utils')
|
||||
const Analytics = require('../Analytics/AnalyticsManager')
|
||||
|
||||
const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})'
|
||||
|
@ -23,27 +23,22 @@ const TokenAccessHandler = {
|
|||
Settings.allowAnonymousReadAndWriteSharing === true,
|
||||
|
||||
READ_AND_WRITE_TOKEN_PATTERN,
|
||||
READ_AND_WRITE_TOKEN_REGEX: new RegExp(`^${READ_AND_WRITE_TOKEN_PATTERN}$`),
|
||||
READ_AND_WRITE_URL_REGEX: new RegExp(`^/${READ_AND_WRITE_TOKEN_PATTERN}$`),
|
||||
|
||||
READ_ONLY_TOKEN_PATTERN,
|
||||
READ_ONLY_TOKEN_REGEX: new RegExp(`^${READ_ONLY_TOKEN_PATTERN}$`),
|
||||
READ_ONLY_URL_REGEX: new RegExp(`^/read/${READ_ONLY_TOKEN_PATTERN}$`),
|
||||
|
||||
makeReadAndWriteTokenUrl(token) {
|
||||
_makeReadAndWriteTokenUrl(token) {
|
||||
return `/${token}`
|
||||
},
|
||||
|
||||
makeReadOnlyTokenUrl(token) {
|
||||
_makeReadOnlyTokenUrl(token) {
|
||||
return `/read/${token}`
|
||||
},
|
||||
|
||||
makeTokenUrl(token) {
|
||||
const tokenType = TokenAccessHandler.getTokenType(token)
|
||||
if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
|
||||
return TokenAccessHandler.makeReadAndWriteTokenUrl(token)
|
||||
return TokenAccessHandler._makeReadAndWriteTokenUrl(token)
|
||||
} else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
|
||||
return TokenAccessHandler.makeReadOnlyTokenUrl(token)
|
||||
return TokenAccessHandler._makeReadOnlyTokenUrl(token)
|
||||
} else {
|
||||
throw new Error('invalid token type')
|
||||
}
|
||||
|
@ -85,24 +80,22 @@ const TokenAccessHandler = {
|
|||
return project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED
|
||||
},
|
||||
|
||||
_projectFindOne(query, callback) {
|
||||
Project.findOne(
|
||||
query,
|
||||
{
|
||||
_id: 1,
|
||||
tokens: 1,
|
||||
publicAccesLevel: 1,
|
||||
owner_ref: 1,
|
||||
name: 1,
|
||||
tokenAccessReadOnly_refs: 1,
|
||||
tokenAccessReadAndWrite_refs: 1,
|
||||
},
|
||||
callback
|
||||
)
|
||||
async _projectFindOne(query) {
|
||||
return await Project.findOne(query, {
|
||||
_id: 1,
|
||||
tokens: 1,
|
||||
publicAccesLevel: 1,
|
||||
owner_ref: 1,
|
||||
name: 1,
|
||||
tokenAccessReadOnly_refs: 1,
|
||||
tokenAccessReadAndWrite_refs: 1,
|
||||
}).exec()
|
||||
},
|
||||
|
||||
getProjectByReadOnlyToken(token, callback) {
|
||||
TokenAccessHandler._projectFindOne({ 'tokens.readOnly': token }, callback)
|
||||
async getProjectByReadOnlyToken(token) {
|
||||
return await TokenAccessHandler._projectFindOne({
|
||||
'tokens.readOnly': token,
|
||||
})
|
||||
},
|
||||
|
||||
_extractNumericPrefix(token) {
|
||||
|
@ -113,92 +106,83 @@ const TokenAccessHandler = {
|
|||
return token.match(/^\d+(\w+)/)
|
||||
},
|
||||
|
||||
getProjectByReadAndWriteToken(token, callback) {
|
||||
async getProjectByReadAndWriteToken(token) {
|
||||
const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token)
|
||||
if (!numericPrefixMatch) {
|
||||
return callback(null, null)
|
||||
return null
|
||||
}
|
||||
const numerics = numericPrefixMatch[1]
|
||||
TokenAccessHandler._projectFindOne(
|
||||
{
|
||||
'tokens.readAndWritePrefix': numerics,
|
||||
},
|
||||
function (err, project) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (project == null) {
|
||||
return callback(null, null)
|
||||
}
|
||||
try {
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
Buffer.from(token),
|
||||
Buffer.from(project.tokens.readAndWrite)
|
||||
)
|
||||
) {
|
||||
logger.err(
|
||||
{ projectId: project._id },
|
||||
'read-and-write token match on numeric section, but not on full token'
|
||||
)
|
||||
callback(null, null)
|
||||
} else {
|
||||
callback(null, project)
|
||||
}
|
||||
} catch (error) {
|
||||
err = error
|
||||
logger.err(
|
||||
{ projectId: project._id, cryptoErr: err },
|
||||
'error comparing tokens'
|
||||
)
|
||||
callback(null, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
getProjectByToken(tokenType, token, callback) {
|
||||
if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
|
||||
TokenAccessHandler.getProjectByReadOnlyToken(token, callback)
|
||||
} else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
|
||||
TokenAccessHandler.getProjectByReadAndWriteToken(token, callback)
|
||||
} else {
|
||||
callback(new Error('invalid token type'))
|
||||
const project = await TokenAccessHandler._projectFindOne({
|
||||
'tokens.readAndWritePrefix': numerics,
|
||||
})
|
||||
|
||||
if (project == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
Buffer.from(token),
|
||||
Buffer.from(project.tokens.readAndWrite)
|
||||
)
|
||||
) {
|
||||
logger.err(
|
||||
{ projectId: project._id },
|
||||
'read-and-write token match on numeric section, but not on full token'
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
return project
|
||||
}
|
||||
} catch (error) {
|
||||
logger.err({ projectId: project._id, error }, 'error comparing tokens')
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
addReadOnlyUserToProject(userId, projectId, callback) {
|
||||
async getProjectByToken(tokenType, token) {
|
||||
if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
|
||||
return await TokenAccessHandler.getProjectByReadOnlyToken(token)
|
||||
} else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
|
||||
return await TokenAccessHandler.getProjectByReadAndWriteToken(token)
|
||||
}
|
||||
throw new Error('invalid token type')
|
||||
},
|
||||
|
||||
async addReadOnlyUserToProject(userId, projectId) {
|
||||
userId = new ObjectId(userId.toString())
|
||||
projectId = new ObjectId(projectId.toString())
|
||||
Analytics.recordEventForUser(userId, 'project-joined', {
|
||||
mode: 'read-only',
|
||||
})
|
||||
Project.updateOne(
|
||||
|
||||
return await Project.updateOne(
|
||||
{
|
||||
_id: projectId,
|
||||
},
|
||||
{
|
||||
$addToSet: { tokenAccessReadOnly_refs: userId },
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
).exec()
|
||||
},
|
||||
|
||||
addReadAndWriteUserToProject(userId, projectId, callback) {
|
||||
async addReadAndWriteUserToProject(userId, projectId) {
|
||||
userId = new ObjectId(userId.toString())
|
||||
projectId = new ObjectId(projectId.toString())
|
||||
Analytics.recordEventForUser(userId, 'project-joined', {
|
||||
mode: 'read-write',
|
||||
})
|
||||
Project.updateOne(
|
||||
|
||||
return await Project.updateOne(
|
||||
{
|
||||
_id: projectId,
|
||||
},
|
||||
{
|
||||
$addToSet: { tokenAccessReadAndWrite_refs: userId },
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
).exec()
|
||||
},
|
||||
|
||||
grantSessionTokenAccess(req, projectId, token) {
|
||||
|
@ -219,65 +203,58 @@ const TokenAccessHandler = {
|
|||
return token
|
||||
},
|
||||
|
||||
validateTokenForAnonymousAccess(projectId, token, callback) {
|
||||
async validateTokenForAnonymousAccess(projectId, token, callback) {
|
||||
if (!token) {
|
||||
return callback(null, false, false)
|
||||
return { isValidReadAndWrite: false, isValidReadOnly: false }
|
||||
}
|
||||
|
||||
const tokenType = TokenAccessHandler.getTokenType(token)
|
||||
if (!tokenType) {
|
||||
return callback(new Error('invalid token type'))
|
||||
throw new Error('invalid token type')
|
||||
}
|
||||
TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
if (
|
||||
!project ||
|
||||
!TokenAccessHandler.tokenAccessEnabledForProject(project) ||
|
||||
project._id.toString() !== projectId.toString()
|
||||
) {
|
||||
return callback(null, false, false)
|
||||
}
|
||||
// TODO: think about cleaning up this interface and its usage in AuthorizationManager
|
||||
callback(
|
||||
null,
|
||||
|
||||
const project = await TokenAccessHandler.getProjectByToken(tokenType, token)
|
||||
|
||||
if (
|
||||
!project ||
|
||||
!TokenAccessHandler.tokenAccessEnabledForProject(project) ||
|
||||
project._id.toString() !== projectId.toString()
|
||||
) {
|
||||
return { isValidReadAndWrite: false, isValidReadOnly: false }
|
||||
}
|
||||
|
||||
// TODO: think about cleaning up this interface and its usage in AuthorizationManager
|
||||
return {
|
||||
isValidReadAndWrite:
|
||||
tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
|
||||
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
|
||||
tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY
|
||||
)
|
||||
})
|
||||
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
|
||||
isValidReadOnly: tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY,
|
||||
}
|
||||
},
|
||||
|
||||
getV1DocPublishedInfo(token, callback) {
|
||||
async getV1DocPublishedInfo(token) {
|
||||
// default to allowing access
|
||||
if (!Settings.apis.v1 || !Settings.apis.v1.url) {
|
||||
return callback(null, { allow: true })
|
||||
return { allow: true }
|
||||
}
|
||||
V1Api.request(
|
||||
{ url: `/api/v1/overleaf/docs/${token}/is_published` },
|
||||
function (err, response, body) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
callback(null, body)
|
||||
}
|
||||
)
|
||||
|
||||
const { body } = await V1Api.promises.request({
|
||||
url: `/api/v1/overleaf/docs/${token}/is_published`,
|
||||
})
|
||||
return body
|
||||
},
|
||||
|
||||
getV1DocInfo(token, v2UserId, callback) {
|
||||
async getV1DocInfo(token, v2UserId) {
|
||||
if (!Settings.apis || !Settings.apis.v1) {
|
||||
return callback(null, {
|
||||
return {
|
||||
exists: true,
|
||||
exported: false,
|
||||
})
|
||||
}
|
||||
const v1Url = `/api/v1/overleaf/docs/${token}/info`
|
||||
V1Api.request({ url: v1Url }, function (err, response, body) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
callback(null, body)
|
||||
})
|
||||
}
|
||||
|
||||
const v1Url = `/api/v1/overleaf/docs/${token}/info`
|
||||
const { body } = await V1Api.promises.request({ url: v1Url })
|
||||
return body
|
||||
},
|
||||
|
||||
createTokenHashPrefix(token) {
|
||||
|
@ -337,19 +314,33 @@ const TokenAccessHandler = {
|
|||
},
|
||||
}
|
||||
|
||||
TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
|
||||
without: [
|
||||
'getTokenType',
|
||||
'tokenAccessEnabledForProject',
|
||||
'_extractNumericPrefix',
|
||||
'_extractStringSuffix',
|
||||
'_projectFindOne',
|
||||
'grantSessionTokenAccess',
|
||||
'getRequestToken',
|
||||
],
|
||||
multiResult: {
|
||||
validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = TokenAccessHandler
|
||||
module.exports = {
|
||||
...TokenAccessHandler,
|
||||
...callbackifyAll(TokenAccessHandler, {
|
||||
multiResult: {
|
||||
validateTokenForAnonymousAccess: [
|
||||
'isValidReadAndWrite',
|
||||
'isValidReadOnly',
|
||||
],
|
||||
},
|
||||
without: [
|
||||
'makeTokenUrl',
|
||||
'getTokenType',
|
||||
'isReadOnlyToken',
|
||||
'isReadAndWriteToken',
|
||||
'isValidToken',
|
||||
'tokenAccessEnabledForProject',
|
||||
'grantSessionTokenAccess',
|
||||
'getRequestToken',
|
||||
'createTokenHashPrefix',
|
||||
'normalizeTokenHashPrefix',
|
||||
'checkTokenHashPrefix',
|
||||
'_makeReadAndWriteTokenUrl',
|
||||
'_makeReadOnlyTokenUrl',
|
||||
'_projectFindOne',
|
||||
'_extractNumericPrefix',
|
||||
'_extractStringSuffix',
|
||||
],
|
||||
}),
|
||||
promises: TokenAccessHandler,
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ describe('TokenAccessHandler', function () {
|
|||
'@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
|
||||
'@overleaf/settings': (this.settings = {}),
|
||||
'../V1/V1Api': (this.V1Api = {
|
||||
request: sinon.stub(),
|
||||
promises: {
|
||||
request: sinon.stub(),
|
||||
},
|
||||
}),
|
||||
crypto: (this.Crypto = require('crypto')),
|
||||
'../Analytics/AnalyticsManager': (this.Analytics = {
|
||||
|
@ -57,19 +59,18 @@ describe('TokenAccessHandler', function () {
|
|||
describe('getProjectByReadOnlyToken', function () {
|
||||
beforeEach(function () {
|
||||
this.token = 'abcdefabcdef'
|
||||
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
})
|
||||
|
||||
it('should get the project', function (done) {
|
||||
this.TokenAccessHandler.getProjectByReadOnlyToken(
|
||||
this.token,
|
||||
(err, project) => {
|
||||
expect(err).to.not.exist
|
||||
expect(project).to.exist
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should get the project', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadOnlyToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -81,69 +82,55 @@ describe('TokenAccessHandler', function () {
|
|||
readAndWrite: this.token,
|
||||
readAndWritePrefix: '1234',
|
||||
}
|
||||
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
|
||||
this.Project.findOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(this.project),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.Crypto.timingSafeEqual.restore()
|
||||
})
|
||||
|
||||
it('should get the project and do timing-safe comparison', function (done) {
|
||||
this.TokenAccessHandler.getProjectByReadAndWriteToken(
|
||||
this.token,
|
||||
(err, project) => {
|
||||
expect(err).to.not.exist
|
||||
expect(project).to.exist
|
||||
expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
|
||||
).to.equal(true)
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should get the project and do timing-safe comparison', async function () {
|
||||
const project =
|
||||
await this.TokenAccessHandler.promises.getProjectByReadAndWriteToken(
|
||||
this.token
|
||||
)
|
||||
expect(project).to.exist
|
||||
expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
|
||||
).to.equal(true)
|
||||
expect(this.Project.findOne.callCount).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addReadOnlyUserToProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon.stub().callsArgWith(2, null)
|
||||
this.Project.updateOne = sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(null),
|
||||
})
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', function (done) {
|
||||
this.TokenAccessHandler.addReadOnlyUserToProject(
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.not.exist
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(
|
||||
this.Project.updateOne.lastCall.args[1].$addToSet
|
||||
).to.have.keys('tokenAccessReadOnly_refs')
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUser,
|
||||
this.userId,
|
||||
'project-joined',
|
||||
{ mode: 'read-only' }
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.projectId
|
||||
)
|
||||
})
|
||||
|
||||
it('should not produce an error', function (done) {
|
||||
this.TokenAccessHandler.addReadOnlyUserToProject(
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadOnly_refs'
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUser,
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.not.exist
|
||||
done()
|
||||
}
|
||||
'project-joined',
|
||||
{ mode: 'read-only' }
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -151,61 +138,47 @@ describe('TokenAccessHandler', function () {
|
|||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, new Error('woops'))
|
||||
.returns({ exec: sinon.stub().rejects(new Error('woops')) })
|
||||
})
|
||||
|
||||
it('should produce an error', function (done) {
|
||||
this.TokenAccessHandler.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.exist
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addReadAndWriteUserToProject', function () {
|
||||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon.stub().callsArgWith(2, null)
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
})
|
||||
|
||||
it('should call Project.updateOne', function (done) {
|
||||
this.TokenAccessHandler.addReadAndWriteUserToProject(
|
||||
it('should call Project.updateOne', async function () {
|
||||
await this.TokenAccessHandler.promises.addReadAndWriteUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.not.exist
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(
|
||||
this.Project.updateOne.lastCall.args[1].$addToSet
|
||||
).to.have.keys('tokenAccessReadAndWrite_refs')
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUser,
|
||||
this.userId,
|
||||
'project-joined',
|
||||
{ mode: 'read-write' }
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.projectId
|
||||
)
|
||||
})
|
||||
|
||||
it('should not produce an error', function (done) {
|
||||
this.TokenAccessHandler.addReadAndWriteUserToProject(
|
||||
expect(this.Project.updateOne.callCount).to.equal(1)
|
||||
expect(
|
||||
this.Project.updateOne.calledWith({
|
||||
_id: this.projectId,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
|
||||
'tokenAccessReadAndWrite_refs'
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.Analytics.recordEventForUser,
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.not.exist
|
||||
done()
|
||||
}
|
||||
'project-joined',
|
||||
{ mode: 'read-write' }
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -213,18 +186,16 @@ describe('TokenAccessHandler', function () {
|
|||
beforeEach(function () {
|
||||
this.Project.updateOne = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, new Error('woops'))
|
||||
.returns({ exec: sinon.stub().rejects(new Error('woops')) })
|
||||
})
|
||||
|
||||
it('should produce an error', function (done) {
|
||||
this.TokenAccessHandler.addReadAndWriteUserToProject(
|
||||
this.userId,
|
||||
this.projectId,
|
||||
err => {
|
||||
expect(err).to.exist
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should produce an error', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.addReadAndWriteUserToProject(
|
||||
this.userId,
|
||||
this.projectId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -234,8 +205,8 @@ describe('TokenAccessHandler', function () {
|
|||
this.req = { session: {}, headers: {} }
|
||||
})
|
||||
|
||||
it('should add the token to the session', function (done) {
|
||||
this.TokenAccessHandler.grantSessionTokenAccess(
|
||||
it('should add the token to the session', function () {
|
||||
this.TokenAccessHandler.promises.grantSessionTokenAccess(
|
||||
this.req,
|
||||
this.projectId,
|
||||
this.token
|
||||
|
@ -243,7 +214,6 @@ describe('TokenAccessHandler', function () {
|
|||
expect(
|
||||
this.req.session.anonTokenAccess[this.projectId.toString()]
|
||||
).to.equal(this.token)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -251,185 +221,158 @@ describe('TokenAccessHandler', function () {
|
|||
describe('when a read-only project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.project)
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, allowed) => {
|
||||
expect(err).to.not.exist
|
||||
expect(
|
||||
this.TokenAccessHandler.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
done()
|
||||
}
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-only access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(true)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should allow read-only access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a read-and-write project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getTokenType = sinon
|
||||
this.TokenAccessHandler.promises.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.project)
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
describe('when Anonymous token access is not enabled', function (done) {
|
||||
describe('when Anonymous token access is not enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(
|
||||
this.TokenAccessHandler.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
done()
|
||||
}
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow read-and-write access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should not allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when anonymous token access is enabled', function (done) {
|
||||
describe('when anonymous token access is enabled', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
|
||||
this.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(
|
||||
this.TokenAccessHandler.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
done()
|
||||
}
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should allow read-and-write access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(true)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should allow read-and-write access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(true)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no project is found', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, null, null)
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, allowed) => {
|
||||
expect(err).to.not.exist
|
||||
expect(
|
||||
this.TokenAccessHandler.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
done()
|
||||
}
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not allow any access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when findProject produces an error', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, new Error('woops'))
|
||||
.rejects(new Error('woops'))
|
||||
})
|
||||
|
||||
it('should try to find projects with both kinds of token', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, allowed) => {
|
||||
expect(err).to.exist
|
||||
expect(allowed).to.not.exist
|
||||
expect(
|
||||
this.TokenAccessHandler.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should try to find projects with both kinds of token', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
|
||||
expect(
|
||||
this.TokenAccessHandler.promises.getProjectByToken.callCount
|
||||
).to.equal(1)
|
||||
})
|
||||
|
||||
it('should produce an error and not allow access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.exist
|
||||
expect(err).to.be.instanceof(Error)
|
||||
expect(rw).to.equal(undefined)
|
||||
expect(ro).to.equal(undefined)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should produce an error and not allow access', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -443,22 +386,20 @@ describe('TokenAccessHandler', function () {
|
|||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readAndWrite')
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.project)
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should not allow any access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -467,66 +408,60 @@ describe('TokenAccessHandler', function () {
|
|||
this.TokenAccessHandler.getTokenType = sinon
|
||||
.stub()
|
||||
.returns('readOnly')
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.project)
|
||||
.resolves(this.project)
|
||||
})
|
||||
|
||||
it('should not allow any access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with nothing', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getProjectByToken = sinon
|
||||
this.TokenAccessHandler.promises.getProjectByToken = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, null, null)
|
||||
.resolves(null)
|
||||
})
|
||||
|
||||
it('should not allow any access', function (done) {
|
||||
this.TokenAccessHandler.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
null,
|
||||
(err, rw, ro) => {
|
||||
expect(err).to.not.exist
|
||||
expect(rw).to.equal(false)
|
||||
expect(ro).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
it('should not allow any access', async function () {
|
||||
const { isValidReadAndWrite, isValidReadOnly } =
|
||||
await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
|
||||
this.projectId,
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(isValidReadAndWrite).to.equal(false)
|
||||
expect(isValidReadOnly).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDocPublishedInfo', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
})
|
||||
|
||||
describe('when v1 api not set', function () {
|
||||
beforeEach(function () {
|
||||
this.settings.apis = { v1: undefined }
|
||||
this.TokenAccessHandler.getV1DocPublishedInfo(this.token, this.callback)
|
||||
})
|
||||
|
||||
it('should not check access and return default info', function () {
|
||||
expect(this.V1Api.request.called).to.equal(false)
|
||||
expect(
|
||||
this.callback.calledWith(null, {
|
||||
allow: true,
|
||||
})
|
||||
).to.equal(true)
|
||||
it('should not check access and return default info', async function () {
|
||||
const info =
|
||||
await this.TokenAccessHandler.promises.getV1DocPublishedInfo(
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(this.V1Api.promises.request.called).to.equal(false)
|
||||
expect(info).to.deep.equal({
|
||||
allow: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -537,63 +472,53 @@ describe('TokenAccessHandler', function () {
|
|||
|
||||
describe('on V1Api.request success', function () {
|
||||
beforeEach(function () {
|
||||
this.V1Api.request = sinon
|
||||
this.V1Api.promises.request = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, null, 'mock-data')
|
||||
this.TokenAccessHandler.getV1DocPublishedInfo(
|
||||
this.token,
|
||||
this.callback
|
||||
)
|
||||
.resolves({ body: 'mock-data' })
|
||||
})
|
||||
|
||||
it('should return response body', function () {
|
||||
it('should return response body', async function () {
|
||||
const info =
|
||||
await this.TokenAccessHandler.promises.getV1DocPublishedInfo(
|
||||
this.token
|
||||
)
|
||||
|
||||
expect(
|
||||
this.V1Api.request.calledWith({
|
||||
this.V1Api.promises.request.calledWith({
|
||||
url: `/api/v1/overleaf/docs/${this.token}/is_published`,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
|
||||
expect(info).to.equal('mock-data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on V1Api.request error', function () {
|
||||
beforeEach(function () {
|
||||
this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
|
||||
this.TokenAccessHandler.getV1DocPublishedInfo(
|
||||
this.token,
|
||||
this.callback
|
||||
)
|
||||
this.V1Api.promises.request = sinon.stub().rejects('error')
|
||||
})
|
||||
|
||||
it('should callback with error', function () {
|
||||
expect(this.callback.calledWith('error')).to.equal(true)
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.getV1DocPublishedInfo(this.token)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getV1DocInfo', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
})
|
||||
|
||||
describe('when v1 api not set', function () {
|
||||
beforeEach(function () {
|
||||
this.TokenAccessHandler.getV1DocInfo(
|
||||
it('should not check access and return default info', async function () {
|
||||
const info = await this.TokenAccessHandler.promises.getV1DocInfo(
|
||||
this.token,
|
||||
this.v2UserId,
|
||||
this.callback
|
||||
this.v2UserId
|
||||
)
|
||||
})
|
||||
|
||||
it('should not check access and return default info', function () {
|
||||
expect(this.V1Api.request.called).to.equal(false)
|
||||
expect(
|
||||
this.callback.calledWith(null, {
|
||||
exists: true,
|
||||
exported: false,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.V1Api.promises.request.called).to.equal(false)
|
||||
expect(info).to.deep.equal({
|
||||
exists: true,
|
||||
exported: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -604,38 +529,38 @@ describe('TokenAccessHandler', function () {
|
|||
|
||||
describe('on V1Api.request success', function () {
|
||||
beforeEach(function () {
|
||||
this.V1Api.request = sinon
|
||||
this.V1Api.promises.request = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, null, 'mock-data')
|
||||
this.TokenAccessHandler.getV1DocInfo(
|
||||
this.token,
|
||||
this.v2UserId,
|
||||
this.callback
|
||||
)
|
||||
.resolves({ body: 'mock-data' })
|
||||
})
|
||||
|
||||
it('should return response body', function () {
|
||||
it('should return response body', async function () {
|
||||
const info = await this.TokenAccessHandler.promises.getV1DocInfo(
|
||||
this.token,
|
||||
this.v2UserId
|
||||
)
|
||||
|
||||
expect(
|
||||
this.V1Api.request.calledWith({
|
||||
this.V1Api.promises.request.calledWith({
|
||||
url: `/api/v1/overleaf/docs/${this.token}/info`,
|
||||
})
|
||||
).to.equal(true)
|
||||
expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
|
||||
expect(info).to.equal('mock-data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on V1Api.request error', function () {
|
||||
beforeEach(function () {
|
||||
this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
|
||||
this.TokenAccessHandler.getV1DocInfo(
|
||||
this.token,
|
||||
this.v2UserId,
|
||||
this.callback
|
||||
)
|
||||
this.V1Api.promises.request = sinon.stub().rejects('error')
|
||||
})
|
||||
|
||||
it('should callback with error', function () {
|
||||
expect(this.callback.calledWith('error')).to.equal(true)
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.TokenAccessHandler.promises.getV1DocInfo(
|
||||
this.token,
|
||||
this.v2UserId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue