Merge branch 'sk-token-csrf-protection'

GitOrigin-RevId: e71f7264be45b665502150e9ffbb85b3fc94665e
This commit is contained in:
Shane Kilkelly
2020-02-25 09:39:53 +00:00
committed by Copybot
parent 995dbc514d
commit 7cbb00f207
19 changed files with 1473 additions and 3118 deletions

View File

@@ -4,10 +4,11 @@ const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const ProjectGetter = require('../Project/ProjectGetter')
const { User } = require('../../models/User')
const PrivilegeLevels = require('./PrivilegeLevels')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const PublicAccessLevels = require('./PublicAccessLevels')
const Errors = require('../Errors/Errors')
const { ObjectId } = require('mongojs')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const { promisify } = require('util')
module.exports = AuthorizationManager = {
isRestrictedUser(userId, privilegeLevel, isTokenMember) {
@@ -163,28 +164,25 @@ module.exports = AuthorizationManager = {
// Anonymous users can have read-only access to token-based projects,
// while read-write access must be logged in,
// unless the `enableAnonymousReadAndWriteSharing` setting is enabled
TokenAccessHandler.isValidToken(projectId, token, function(
err,
isValidReadAndWrite,
isValidReadOnly
) {
if (err) {
return callback(err)
TokenAccessHandler.validateTokenForAnonymousAccess(
projectId,
token,
function(err, isValidReadAndWrite, isValidReadOnly) {
if (err) {
return callback(err)
}
if (isValidReadOnly) {
// Grant anonymous user read-only access
return callback(null, PrivilegeLevels.READ_ONLY, false, false)
}
if (isValidReadAndWrite) {
// Grant anonymous user read-and-write access
return callback(null, PrivilegeLevels.READ_AND_WRITE, false, false)
}
// Deny anonymous access
callback(null, PrivilegeLevels.NONE, false, false)
}
if (isValidReadOnly) {
// Grant anonymous user read-only access
return callback(null, PrivilegeLevels.READ_ONLY, false, false)
}
if (
isValidReadAndWrite &&
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
) {
// Grant anonymous user read-and-write access
return callback(null, PrivilegeLevels.READ_AND_WRITE, false, false)
}
// Deny anonymous access
callback(null, PrivilegeLevels.NONE, false, false)
})
)
},
canUserReadProject(userId, projectId, token, callback) {
@@ -280,3 +278,9 @@ module.exports = AuthorizationManager = {
})
}
}
AuthorizationManager.promises = {
getPrivilegeLevelForProject: promisify(
AuthorizationManager.getPrivilegeLevelForProject
)
}

View File

@@ -1,6 +1,8 @@
module.exports = {
const PrivilegeLevels = {
NONE: false,
READ_ONLY: 'readOnly',
READ_AND_WRITE: 'readAndWrite',
OWNER: 'owner'
}
module.exports = PrivilegeLevels

View File

@@ -736,12 +736,15 @@ const ProjectController = {
const { subscription } = results
const { brandVariation } = results
const token = TokenAccessHandler.getRequestToken(req, projectId)
const anonRequestToken = TokenAccessHandler.getRequestToken(
req,
projectId
)
const { isTokenMember } = results
AuthorizationManager.getPrivilegeLevelForProject(
userId,
projectId,
token,
anonRequestToken,
(error, privilegeLevel) => {
let allowedFreeTrial
if (error != null) {
@@ -804,7 +807,7 @@ const ProjectController = {
privilegeLevel,
chatUrl: Settings.apis.chat.url,
anonymous,
anonymousAccessToken: req._anonymousAccessToken,
anonymousAccessToken: anonymous ? anonRequestToken : null,
isTokenMember,
isRestrictedTokenMember: AuthorizationManager.isRestrictedUser(
userId,
@@ -931,7 +934,6 @@ const ProjectController = {
archived,
trashed,
owner_ref: project.owner_ref,
tokens: project.tokens,
isV1Project: false
}
if (accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN) {

View File

@@ -1,277 +1,306 @@
/* eslint-disable
camelcase,
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let TokenAccessController
const ProjectController = require('../Project/ProjectController')
const AuthenticationController = require('../Authentication/AuthenticationController')
const TokenAccessHandler = require('./TokenAccessHandler')
const Errors = require('../Errors/Errors')
const logger = require('logger-sharelatex')
const settings = require('settings-sharelatex')
const OError = require('@overleaf/o-error')
const { expressify } = require('../../util/promises')
const AuthorizationManager = require('../Authorization/AuthorizationManager')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
module.exports = TokenAccessController = {
_loadEditor(projectId, req, res, next) {
req.params.Project_id = projectId.toString()
return ProjectController.loadEditor(req, res, next)
},
const orderedPrivilegeLevels = [
PrivilegeLevels.NONE,
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.OWNER
]
_tryHigherAccess(token, userId, req, res, next) {
return TokenAccessHandler.findProjectWithHigherAccess(
async function _userAlreadyHasHigherPrivilege(
userId,
projectId,
token,
tokenType
) {
if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
throw new Error('bad token type')
}
const privilegeLevel = await AuthorizationManager.promises.getPrivilegeLevelForProject(
userId,
projectId,
token
)
return (
orderedPrivilegeLevels.indexOf(privilegeLevel) >=
orderedPrivilegeLevels.indexOf(tokenType)
)
}
const makePostUrl = token => {
if (TokenAccessHandler.isReadAndWriteToken(token)) {
return `/${token}/grant`
} else if (TokenAccessHandler.isReadOnlyToken(token)) {
return `/read/${token}/grant`
} else {
throw new Error('invalid token type')
}
}
async function _handleV1Project(token, userId) {
if (!userId) {
return { v1Import: { status: 'mustLogin' } }
} else {
const docInfo = await TokenAccessHandler.promises.getV1DocInfo(
token,
userId,
function(err, project) {
if (err != null) {
logger.warn(
{ err, token, userId },
'[TokenAccess] error finding project with higher access'
)
return next(err)
}
if (project == null) {
logger.log(
{ token, userId },
'[TokenAccess] no project with higher access found for this user and token'
)
return next(new Errors.NotFoundError())
}
logger.log(
{ token, userId, projectId: project._id },
'[TokenAccess] user has higher access to project, redirecting'
)
return res.redirect(302, `/project/${project._id}`)
}
userId
)
},
readAndWriteToken(req, res, next) {
const userId = AuthenticationController.getLoggedInUserId(req)
const token = req.params['read_and_write_token']
logger.log(
{ userId, token },
'[TokenAccess] requesting read-and-write token access'
)
return TokenAccessHandler.findProjectWithReadAndWriteToken(token, function(
err,
project,
projectExists
) {
if (err != null) {
logger.warn(
{ err, token, userId },
'[TokenAccess] error getting project by readAndWrite token'
)
return next(err)
if (!docInfo) {
return { v1Import: { status: 'cannotImport' } }
}
if (!docInfo.exists) {
return null
}
if (docInfo.exported) {
return null
}
return {
v1Import: {
status: 'canImport',
projectId: token,
hasOwner: docInfo.has_owner,
name: docInfo.name || 'Untitled',
hasAssignment: docInfo.has_assignment,
brandInfo: docInfo.brand_info
}
if (!projectExists && settings.overleaf) {
logger.log(
{ token, userId },
'[TokenAccess] no project found for this token'
)
return TokenAccessController._handleV1Project(
token,
userId,
`/${token}`,
res,
next
)
} else if (project == null) {
if (userId == null) {
return next(new Errors.NotFoundError())
}
return TokenAccessController._tryHigherAccess(
token,
userId,
req,
res,
next
)
} else {
if (userId == null) {
if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
TokenAccessHandler.grantSessionTokenAccess(req, project._id, token)
req._anonymousAccessToken = token
return TokenAccessController._loadEditor(
project._id,
req,
res,
next
)
} else {
logger.log(
{ token, projectId: project._id },
'[TokenAccess] deny anonymous read-and-write token access'
)
AuthenticationController.setRedirectInSession(req)
return res.redirect('/restricted')
}
}
if (project.owner_ref.toString() === userId) {
logger.log(
{ userId, projectId: project._id },
'[TokenAccess] user is already project owner'
)
return TokenAccessController._loadEditor(project._id, req, res, next)
}
logger.log(
{ userId, projectId: project._id },
'[TokenAccess] adding user to project with readAndWrite token'
)
return TokenAccessHandler.addReadAndWriteUserToProject(
userId,
project._id,
function(err) {
if (err != null) {
logger.warn(
{ err, token, userId, projectId: project._id },
'[TokenAccess] error adding user to project with readAndWrite token'
)
return next(err)
}
return TokenAccessController._loadEditor(
project._id,
req,
res,
next
)
}
)
}
})
},
readOnlyToken(req, res, next) {
const userId = AuthenticationController.getLoggedInUserId(req)
const token = req.params['read_only_token']
return TokenAccessHandler.getV1DocPublishedInfo(token, function(
err,
doc_published_info
) {
if (err != null) {
return next(err)
}
if (doc_published_info.allow === false) {
return res.redirect(doc_published_info.published_path)
}
return TokenAccessHandler.findProjectWithReadOnlyToken(token, function(
err,
project,
projectExists
) {
if (err != null) {
logger.warn(
{ err, token, userId },
'[TokenAccess] error getting project by readOnly token'
)
return next(err)
}
if (!projectExists && settings.overleaf) {
logger.log(
{ token, userId },
'[TokenAccess] no project found for this token'
)
return TokenAccessController._handleV1Project(
token,
userId,
`/read/${token}`,
res,
next
)
} else if (project == null) {
if (userId == null) {
return next(new Errors.NotFoundError())
}
return TokenAccessController._tryHigherAccess(
token,
userId,
req,
res,
next
)
} else {
if (userId == null) {
TokenAccessHandler.grantSessionTokenAccess(req, project._id, token)
req._anonymousAccessToken = token
return TokenAccessController._loadEditor(
project._id,
req,
res,
next
)
} else {
if (project.owner_ref.toString() === userId) {
return TokenAccessController._loadEditor(
project._id,
req,
res,
next
)
}
logger.log(
{ userId, projectId: project._id },
'[TokenAccess] adding user to project with readOnly token'
)
return TokenAccessHandler.addReadOnlyUserToProject(
userId,
project._id,
function(err) {
if (err != null) {
logger.warn(
{ err, token, userId, projectId: project._id },
'[TokenAccess] error adding user to project with readAndWrite token'
)
return next(err)
}
return TokenAccessController._loadEditor(
project._id,
req,
res,
next
)
}
)
}
}
})
})
},
_handleV1Project(token, userId, redirectPath, res, next) {
if (userId == null) {
return res.render('project/v2-import', { loginRedirect: redirectPath })
} else {
TokenAccessHandler.getV1DocInfo(token, userId, function(err, doc_info) {
if (err != null) {
return next(err)
}
if (!doc_info) {
res.status(400)
return res.render('project/cannot-import-v1-project')
}
if (!doc_info.exists) {
return next(new Errors.NotFoundError())
}
if (doc_info.exported) {
return next(new Errors.NotFoundError())
}
return res.render('project/v2-import', {
projectId: token,
hasOwner: doc_info.has_owner,
name: doc_info.name || 'Untitled',
hasAssignment: doc_info.has_assignment,
brandInfo: doc_info.brand_info
})
})
}
}
}
async function tokenAccessPage(req, res, next) {
const { token } = req.params
if (!TokenAccessHandler.isValidToken(token)) {
return next(new Errors.NotFoundError())
}
try {
if (TokenAccessHandler.isReadOnlyToken(token)) {
const docPublishedInfo = await TokenAccessHandler.promises.getV1DocPublishedInfo(
token
)
if (docPublishedInfo.allow === false) {
return res.redirect(302, docPublishedInfo.published_path)
}
}
res.render('project/token/access', {
postUrl: makePostUrl(token)
})
} catch (err) {
return next(
new OError({
message: 'error while rendering token access page',
info: { token }
}).withCause(err)
)
}
}
async function checkAndGetProjectOrResponseAction(
tokenType,
token,
userId,
req,
res,
next
) {
// Try to get the project, and/or an alternative action to take.
// Returns a tuple of [project, action]
const project = await TokenAccessHandler.promises.getProjectByToken(
tokenType,
token
)
if (!project) {
if (settings.overleaf) {
const v1ImportData = await _handleV1Project(token, userId)
return [
null,
() => {
if (v1ImportData) {
res.json(v1ImportData)
} else {
res.sendStatus(404)
}
}
]
} else {
return [null, null]
}
}
const projectId = project._id
const isAnonymousUser = !userId
const tokenAccessEnabled = TokenAccessHandler.tokenAccessEnabledForProject(
project
)
if (isAnonymousUser && tokenAccessEnabled) {
if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
logger.info({ projectId }, 'granting read-write anonymous access')
TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
return [
null,
() => {
res.json({
redirect: `/project/${projectId}`,
grantAnonymousAccess: tokenType
})
}
]
} else {
logger.warn(
{ token, projectId },
'[TokenAccess] deny anonymous read-and-write token access'
)
AuthenticationController.setRedirectInSession(req)
return [
null,
() => {
res.json({
redirect: '/restricted',
anonWriteAccessDenied: true
})
}
]
}
} else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
logger.info({ projectId }, 'granting read-only anonymous access')
TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
return [
null,
() => {
res.json({
redirect: `/project/${projectId}`,
grantAnonymousAccess: tokenType
})
}
]
} else {
throw new Error('unreachable')
}
}
const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
userId,
projectId,
token,
tokenType
)
if (userHasPrivilege) {
return [
null,
() => {
res.json({ redirect: `/project/${project._id}`, higherAccess: true })
}
]
}
if (!tokenAccessEnabled) {
return [
null,
() => {
next(new Errors.NotFoundError())
}
]
}
return [project, null]
}
async function grantTokenAccessReadAndWrite(req, res, next) {
const { token } = req.params
const userId = AuthenticationController.getLoggedInUserId(req)
if (!TokenAccessHandler.isReadAndWriteToken(token)) {
return res.sendStatus(400)
}
const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
try {
const [project, action] = await checkAndGetProjectOrResponseAction(
tokenType,
token,
userId,
req,
res,
next
)
if (action) {
return action()
}
if (!project) {
return next(new Errors.NotFoundError())
}
await TokenAccessHandler.promises.addReadAndWriteUserToProject(
userId,
project._id
)
return res.json({
redirect: `/project/${project._id}`,
tokenAccessGranted: tokenType
})
} catch (err) {
return next(
new OError({
message: 'error while trying to grant read-and-write token access',
info: { token }
}).withCause(err)
)
}
}
async function grantTokenAccessReadOnly(req, res, next) {
const { token } = req.params
const userId = AuthenticationController.getLoggedInUserId(req)
if (!TokenAccessHandler.isReadOnlyToken(token)) {
return res.sendStatus(400)
}
const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY
const docPublishedInfo = await TokenAccessHandler.promises.getV1DocPublishedInfo(
token
)
if (docPublishedInfo.allow === false) {
return res.json({ redirect: docPublishedInfo.published_path })
}
try {
const [project, action] = await checkAndGetProjectOrResponseAction(
tokenType,
token,
userId,
req,
res,
next
)
if (action) {
return action()
}
if (!project) {
return next(new Errors.NotFoundError())
}
await TokenAccessHandler.promises.addReadOnlyUserToProject(
userId,
project._id
)
return res.json({
redirect: `/project/${project._id}`,
tokenAccessGranted: tokenType
})
} catch (err) {
return next(
new OError({
message: 'error while trying to grant read-only token access',
info: { token }
}).withCause(err)
)
}
}
module.exports = {
READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
tokenAccessPage: expressify(tokenAccessPage),
grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite)
}

View File

@@ -1,5 +1,4 @@
const { Project } = require('../../models/Project')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const UserGetter = require('../User/UserGetter')
@@ -8,63 +7,100 @@ const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const V1Api = require('../V1/V1Api')
const crypto = require('crypto')
const { promisifyAll } = require('../../util/promises')
const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})'
const READ_ONLY_TOKEN_PATTERN = '([a-z]{12})'
const TokenAccessHandler = {
TOKEN_TYPES: {
READ_ONLY: PrivilegeLevels.READ_ONLY,
READ_AND_WRITE: PrivilegeLevels.READ_AND_WRITE
},
ANONYMOUS_READ_AND_WRITE_ENABLED:
Settings.allowAnonymousReadAndWriteSharing === true,
READ_AND_WRITE_TOKEN_REGEX: /^(\d+)(\w+)$/,
READ_AND_WRITE_URL_REGEX: /^\/(\d+)(\w+)$/,
READ_ONLY_TOKEN_REGEX: /^([a-z]{12})$/,
READ_ONLY_URL_REGEX: /^\/read\/([a-z]{12})$/,
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}$`),
getTokenType(token) {
if (!token) {
return null
}
if (token.match(`^${TokenAccessHandler.READ_ONLY_TOKEN_PATTERN}$`)) {
return TokenAccessHandler.TOKEN_TYPES.READ_ONLY
} else if (
token.match(`^${TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN}$`)
) {
return TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
}
return null
},
isReadOnlyToken(token) {
return (
TokenAccessHandler.getTokenType(token) ===
TokenAccessHandler.TOKEN_TYPES.READ_ONLY
)
},
isReadAndWriteToken(token) {
return (
TokenAccessHandler.getTokenType(token) ===
TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
)
},
isValidToken(token) {
return TokenAccessHandler.getTokenType(token) != null
},
tokenAccessEnabledForProject(project) {
return project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED
},
_projectFindOne(query, callback) {
Project.findOne(
query,
{
_id: 1,
tokens: 1,
publicAccesLevel: 1,
owner_ref: 1,
name: 1
},
callback
)
},
getProjectByReadOnlyToken(token, callback) {
TokenAccessHandler._projectFindOne({ 'tokens.readOnly': token }, callback)
},
_extractNumericPrefix(token) {
return token.match(/^(\d+)\w+/)
},
_getProjectByReadOnlyToken(token, callback) {
Project.findOne(
{
'tokens.readOnly': token
},
{ _id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1 },
callback
)
_extractStringSuffix(token) {
return token.match(/^\d+(\w+)/)
},
_getProjectByEitherToken(token, callback) {
TokenAccessHandler._getProjectByReadOnlyToken(token, function(
err,
project
) {
if (err != null) {
return callback(err)
}
if (project != null) {
return callback(null, project)
}
TokenAccessHandler._getProjectByReadAndWriteToken(token, function(
err,
project
) {
if (err != null) {
return callback(err)
}
callback(null, project)
})
})
},
_getProjectByReadAndWriteToken(token, callback) {
getProjectByReadAndWriteToken(token, callback) {
const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token)
if (!numericPrefixMatch) {
return callback(null, null)
}
const numerics = numericPrefixMatch[1]
Project.findOne(
TokenAccessHandler._projectFindOne(
{
'tokens.readAndWritePrefix': numerics
},
{ _id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1 },
function(err, project) {
if (err != null) {
return callback(err)
@@ -96,69 +132,14 @@ const TokenAccessHandler = {
)
},
findProjectWithReadOnlyToken(token, callback) {
TokenAccessHandler._getProjectByReadOnlyToken(token, function(
err,
project
) {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback(null, null, false) // Project doesn't exist, so we handle differently
}
if (project.publicAccesLevel !== PublicAccessLevels.TOKEN_BASED) {
return callback(null, null, true) // Project does exist, but it isn't token based
}
callback(null, project, true)
})
},
findProjectWithReadAndWriteToken(token, callback) {
TokenAccessHandler._getProjectByReadAndWriteToken(token, function(
err,
project
) {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback(null, null, false) // Project doesn't exist, so we handle differently
}
if (project.publicAccesLevel !== PublicAccessLevels.TOKEN_BASED) {
return callback(null, null, true) // Project does exist, but it isn't token based
}
callback(null, project, true)
})
},
_userIsMember(userId, projectId, callback) {
CollaboratorsGetter.isUserInvitedMemberOfProject(
userId,
projectId,
callback
)
},
findProjectWithHigherAccess(token, userId, callback) {
TokenAccessHandler._getProjectByEitherToken(token, function(err, project) {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback(null, null)
}
const projectId = project._id
TokenAccessHandler._userIsMember(userId, projectId, function(
err,
isMember
) {
if (err != null) {
return callback(err)
}
callback(null, isMember === true ? project : 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 {
return callback(new Error('invalid token type'))
}
},
addReadOnlyUserToProject(userId, projectId, callback) {
@@ -196,7 +177,7 @@ const TokenAccessHandler = {
if (!req.session.anonTokenAccess) {
req.session.anonTokenAccess = {}
}
req.session.anonTokenAccess[projectId.toString()] = token.toString()
req.session.anonTokenAccess[projectId.toString()] = token
},
getRequestToken(req, projectId) {
@@ -208,32 +189,32 @@ const TokenAccessHandler = {
return token
},
isValidToken(projectId, token, callback) {
validateTokenForAnonymousAccess(projectId, token, callback) {
if (!token) {
return callback(null, false, false)
}
const _validate = project =>
project != null &&
project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED &&
project._id.toString() === projectId.toString()
TokenAccessHandler.findProjectWithReadAndWriteToken(token, function(
err,
readAndWriteProject
) {
if (err != null) {
const tokenType = TokenAccessHandler.getTokenType(token)
if (!tokenType) {
return callback(new Error('invalid token type'))
}
TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => {
if (err) {
return callback(err)
}
const isValidReadAndWrite = _validate(readAndWriteProject)
TokenAccessHandler.findProjectWithReadOnlyToken(token, function(
err,
readOnlyProject
if (
!project ||
!TokenAccessHandler.tokenAccessEnabledForProject(project) ||
project._id.toString() !== projectId.toString()
) {
if (err != null) {
return callback(err)
}
const isValidReadOnly = _validate(readOnlyProject)
callback(null, isValidReadAndWrite, isValidReadOnly)
})
return callback(null, false, false)
}
// TODO: think about cleaning up this interface and its usage in AuthorizationManager
return callback(
null,
tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY
)
})
},
@@ -297,4 +278,18 @@ const TokenAccessHandler = {
}
}
TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
without: [
'getTokenType',
'tokenAccessEnabledForProject',
'_extractNumericPrefix',
'_extractStringSuffix',
'_projectFindOne',
'grantSessionTokenAccess',
'getRequestToken',
'protectTokens',
'validateTokenForAnonymousAccess'
]
})
module.exports = TokenAccessHandler

View File

@@ -10,10 +10,10 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let V1Api
const request = require('request')
const settings = require('settings-sharelatex')
const Errors = require('../Errors/Errors')
const { promisifyAll } = require('../../util/promises')
// TODO: check what happens when these settings aren't defined
const DEFAULT_V1_PARAMS = {
@@ -36,7 +36,7 @@ const DEFAULT_V1_OAUTH_PARAMS = {
const v1OauthRequest = request.defaults(DEFAULT_V1_OAUTH_PARAMS)
module.exports = V1Api = {
const V1Api = {
request(options, callback) {
if (callback == null) {
return request(options)
@@ -103,3 +103,11 @@ module.exports = V1Api = {
}
}
}
V1Api.promises = promisifyAll(V1Api, {
multiResult: {
request: ['response', 'body'],
oauthRequest: ['response', 'body']
}
})
module.exports = V1Api

View File

@@ -1037,23 +1037,43 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
})
webRouter.get(
'/read/:read_only_token([a-z]+)',
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})`,
RateLimiterMiddleware.rateLimit({
endpointName: 'read-only-token',
maxRequests: 15,
timeInterval: 60
}),
TokenAccessController.readOnlyToken
TokenAccessController.tokenAccessPage
)
webRouter.get(
'/:read_and_write_token([0-9]+[a-z]+)',
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})`,
RateLimiterMiddleware.rateLimit({
endpointName: 'read-and-write-token',
maxRequests: 15,
timeInterval: 60
}),
TokenAccessController.readAndWriteToken
TokenAccessController.tokenAccessPage
)
webRouter.post(
`/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})/grant`,
RateLimiterMiddleware.rateLimit({
endpointName: 'grant-token-access-read-write',
maxRequests: 10,
timeInterval: 60
}),
TokenAccessController.grantTokenAccessReadAndWrite
)
webRouter.post(
`/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})/grant`,
RateLimiterMiddleware.rateLimit({
endpointName: 'grant-token-access-read-only',
maxRequests: 10,
timeInterval: 60
}),
TokenAccessController.grantTokenAccessReadOnly
)
webRouter.get('*', ErrorController.notFound)

View File

@@ -0,0 +1,155 @@
extends ../../layout
block vars
- metadata = { viewport: true }
block content
script(type="template", id="overleaf-token-access-data")!= StringHelper.stringifyJsonForScript({ postUrl: postUrl, csrfToken: csrfToken})
div(
ng-controller="TokenAccessPageController",
ng-init="post()"
)
.editor.full-size
div
|  
a(href="/project" style="font-size: 2rem; margin-left: 1rem; color: #ddd;")
i.fa.fa-arrow-left
.loading-screen(
ng-show="mode == 'accessAttempt'"
)
.loading-screen-brand-container
.loading-screen-brand()
h3.loading-screen-label.text-center
| #{translate('join_project')}
span(ng-show="accessInFlight == true")
span.loading-screen-ellip .
span.loading-screen-ellip .
span.loading-screen-ellip .
.global-alerts.text-center(ng-cloak)
div(ng-show="accessError", ng-cloak)
br
div(ng-switch="accessError", ng-cloak)
div(ng-switch-when="not_found")
h4(aria-live="assertive")
| Project not found
div(ng-switch-default)
.alert.alert-danger(aria-live="assertive") #{translate('token_access_failure')}
p
a(href="/") #{translate('home')}
.loading-screen(
ng-show="mode == 'v1Import'"
)
.container
.row
.col-sm-8.col-sm-offset-2
h1.text-center Move project to Overleaf v2
img.v2-import__img(
src="/img/v1-import/v2-editor.png"
alt="The new V2 editor."
)
div(ng-if="v1ImportData.status == 'cannotImport'")
h2.text-center
| Cannot Access Overleaf v1 Project
p.text-center.row-spaced-small
| The project you are attempting to access must be imported to Overleaf v2 before it can be accessed. Please contact the project owner or
|
a(href="/contact") contact support
|
| for assistance.
div(ng-if="v1ImportData.status == 'mustLogin'")
p.text-center.row-spaced-small
| This project has not yet been moved into the new version of
| Overleaf. You will need to log in and move it in order to
| continue working on it.
.row-spaced.text-center
a.btn.btn-primary(
href="/login?redir={{ currentPath() }}"
) Log In To Move Project
div(ng-if="v1ImportData.status == 'canImport'")
div(ng-if="v1ImportData.hasOwner")
p.text-center.row-spaced-small
| #[strong() {{ getProjectName() }}] has not yet been moved into
| the new version of Overleaf. You will need to move it in order
| to continue working on it. It should only take a few seconds.
form(
async-form="v2Import"
name="v2ImportForm"
action="{{ buildFormImportPath(v1ImportData.projectId) }}"
method="POST"
ng-cloak
)
input(name='_csrf', type='hidden', value=csrfToken)
form-messages(for="v2ImportForm")
input.row-spaced.btn.btn-primary.text-center.center-block(
type="submit"
ng-value="v2ImportForm.inflight || v2ImportForm.response.success ? 'Moving to v2...' : 'Move Project and Continue'"
ng-disabled="v2ImportForm.inflight || v2ImportForm.response.success"
)
.row-spaced-small.text-center
a(href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
| Download project zip file
div(ng-if="!v1ImportData.hasOwner")
p.text-center.row-spaced.small
| #[strong() {{ getProjectName() }}] has not yet been moved into
| the new version of Overleaf. This project was created
| anonymously and therefore cannot be automatically imported.
| Please download a zip file of the project and upload that to
| continue editing it. If you would like to delete this project
| after you have made a copy, please contact support.
.row-spaced.text-center
a.btn.btn-primary(href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
| Download project zip file
.row-spaced.text-center
div(ng-if="v1ImportData.hasAssignment")
p
| #[span.fa.fa-exclamation-triangle]
| This project is an assignment, and the assignment toolkit is
| no longer supported in Overleaf v2. When you move it to
| Overleaf v2, it will become a normal project.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#assignment-tools") Please see our FAQ]
| for more information or contact your instructor if you haven't
| already submitted it.
div(ng-if="!v1ImportData.hasAssignment")
div(ng-switch="v1ImportData.brandInfo")
div(ng-switch-when="'f1000'")
p
| #[span.fa.fa-exclamation-triangle]
| This project is an F1000Research article, and our integration
| with F1000Research has changed in Overleaf v2.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#f1000research") Find out more about moving to Overleaf v2]
div(ng-switch-when="'wellcome'")
p
| #[span.fa.fa-exclamation-triangle]
| This project is an Wellcome Open Research article, and our
| integration with Wellcome Open Research has changed in
| Overleaf v2.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#f1000research") Find out more about moving to Overleaf v2]
div(ng-switch-default)
a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ")
| Find out more about moving to Overleaf v2
block append foot-scripts
script.
$(document).ready(function () {
setTimeout(function() {
$('.loading-screen-brand').css('height', '20%')
}, 500);
});

View File

@@ -1,88 +0,0 @@
extends ../layout
block vars
- metadata = { viewport: true }
block content
main.content
.container
.row
.col-sm-8.col-sm-offset-2
h1.text-center Move project to Overleaf v2
img.v2-import__img(
src="/img/v1-import/v2-editor.png"
alt="The new V2 editor."
)
if loginRedirect
p.text-center.row-spaced-small
| This project has not yet been moved into the new version of
| Overleaf. You will need to log in and move it in order to
| continue working on it.
.row-spaced.text-center
a.btn.btn-primary(
href="/login?redir=" + loginRedirect
) Log In To Move Project
else
if hasOwner
p.text-center.row-spaced-small
| #[strong(ng-non-bindable) #{name}] has not yet been moved into
| the new version of Overleaf. You will need to move it in order
| to continue working on it. It should only take a few seconds.
form(
async-form="v2Import"
name="v2ImportForm"
action="/overleaf/project/"+ projectId + "/import"
method="POST"
ng-cloak
)
input(name='_csrf', type='hidden', value=csrfToken)
form-messages(for="v2ImportForm")
input.row-spaced.btn.btn-primary.text-center.center-block(
type="submit"
ng-value="v2ImportForm.inflight || v2ImportForm.response.success ? 'Moving to v2...' : 'Move Project and Continue'"
ng-disabled="v2ImportForm.inflight || v2ImportForm.response.success"
)
.row-spaced-small.text-center
a(href="/overleaf/project/" + projectId + "/download/zip") Download project zip file
else
p.text-center.row-spaced.small
| #[strong(ng-non-bindable) #{name}] has not yet been moved into
| the new version of Overleaf. This project was created
| anonymously and therefore cannot be automatically imported.
| Please download a zip file of the project and upload that to
| continue editing it. If you would like to delete this project
| after you have made a copy, please contact support.
.row-spaced.text-center
a.btn.btn-primary(href="/overleaf/project/" + projectId + "/download/zip") Download project zip file
.row-spaced.text-center
if hasAssignment
p
| #[span.fa.fa-exclamation-triangle]
| This project is an assignment, and the assignment toolkit is
| no longer supported in Overleaf v2. When you move it to
| Overleaf v2, it will become a normal project.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#assignment-tools") Please see our FAQ]
| for more information or contact your instructor if you haven't
| already submitted it.
else if brandInfo == 'f1000'
p
| #[span.fa.fa-exclamation-triangle]
| This project is an F1000Research article, and our integration
| with F1000Research has changed in Overleaf v2.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#f1000research") Find out more about moving to Overleaf v2]
else if brandInfo == 'wellcome'
p
| #[span.fa.fa-exclamation-triangle]
| This project is an Wellcome Open Research article, and our
| integration with Wellcome Open Research has changed in
| Overleaf v2.
| #[a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ#f1000research") Find out more about moving to Overleaf v2]
else
a(href="https://www.overleaf.com/learn/how-to/Overleaf_v2_FAQ")
| Find out more about moving to Overleaf v2

View File

@@ -29,6 +29,7 @@ services:
ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file,mendeley,zotero'
MOCHA_GREP: ${MOCHA_GREP}
NODE_ENV: test
# SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true'
SHARELATEX_CONFIG:
command: npm run test:acceptance:app
depends_on:

View File

@@ -10,6 +10,7 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define([
'main/token-access',
'main/project-list/index',
'main/account-settings',
'main/clear-sessions',

View File

@@ -865,19 +865,7 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
ProjectListService
) {
$scope.projectLink = function(project) {
if (
project.accessLevel === 'readAndWrite' &&
project.source === 'token'
) {
return `/${project.tokens.readAndWrite}`
} else if (
project.accessLevel === 'readOnly' &&
project.source === 'token'
) {
return `/read/${project.tokens.readOnly}`
} else {
return `/project/${project.id}`
}
return `/project/${project.id}`
}
$scope.isLinkSharingProject = project => project.source === 'token'

View File

@@ -0,0 +1,83 @@
define(['base'], App => {
App.controller(
'TokenAccessPageController',
($scope, $http, $location, localStorage) => {
window.S = $scope
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import'
$scope.v1ImportData = null
$scope.accessInFlight = false
$scope.accessSuccess = false
$scope.accessError = false
$scope.currentPath = () => {
return $location.path()
}
$scope.buildFormImportPath = projectId => {
return `/overleaf/project/${projectId}/import`
}
$scope.buildZipDownloadPath = projectId => {
return `/overleaf/project/${projectId}/download/zip`
}
$scope.getProjectName = () => {
if (!$scope.v1ImportData || !$scope.v1ImportData.name) {
return 'This project'
} else {
return $scope.v1ImportData.name
}
}
$scope.post = () => {
$scope.mode = 'accessAttempt'
const textData = $('#overleaf-token-access-data').text()
let parsedData = JSON.parse(textData)
const { postUrl, csrfToken } = parsedData
$scope.accessInFlight = true
$http({
method: 'POST',
url: postUrl,
data: {
_csrf: csrfToken
}
}).then(
function successCallback(response) {
$scope.accessInFlight = false
$scope.accessError = false
const { data } = response
if (data.redirect) {
const redirect = response.data.redirect
if (!redirect) {
console.warn(
'no redirect supplied in success response data',
response
)
$scope.accessError = true
return
}
window.location.replace(redirect)
} else if (data.v1Import) {
$scope.mode = 'v1Import'
$scope.v1ImportData = data.v1Import
} else {
console.warn(
'invalid data from server in success response',
response
)
$scope.accessError = true
}
},
function errorCallback(response) {
console.warn('error response from server', response)
$scope.accessInFlight = false
$scope.accessError = response.status === 404 ? 'not_found' : 'error'
}
)
}
}
)
})

View File

@@ -6,7 +6,7 @@ const request = require('./helpers/request')
const settings = require('settings-sharelatex')
const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs')
const tryReadAccess = (user, projectId, test, callback) =>
const tryEditorAccess = (user, projectId, test, callback) =>
async.series(
[
cb =>
@@ -32,23 +32,69 @@ const tryReadAccess = (user, projectId, test, callback) =>
callback
)
const tryReadOnlyTokenAccess = (user, token, test, callback) =>
user.request.get(`/read/${token}`, (error, response, body) => {
if (error != null) {
return callback(error)
}
test(response, body)
callback()
})
const tryReadOnlyTokenAccess = (
user,
token,
testPageLoad,
testFormPost,
callback
) => {
_doTryTokenAccess(
`/read/${token}`,
user,
token,
testPageLoad,
testFormPost,
callback
)
}
const tryReadAndWriteTokenAccess = (user, token, test, callback) =>
user.request.get(`/${token}`, (error, response, body) => {
if (error != null) {
return callback(error)
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)
}
test(response, body)
callback()
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 tryContentAccess = (user, projcetId, test, callback) => {
// The real-time service calls this end point to determine the user's
@@ -122,8 +168,16 @@ describe('TokenAccess', function() {
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),
@@ -153,7 +207,7 @@ describe('TokenAccess', function() {
async.series(
[
cb => {
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -210,7 +264,7 @@ describe('TokenAccess', function() {
[
cb => {
// deny access before token is used
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -228,6 +282,11 @@ describe('TokenAccess', function() {
(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
)
},
@@ -248,6 +307,51 @@ describe('TokenAccess', function() {
},
cb
)
},
cb => {
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
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)
},
cb
)
}
],
done
@@ -264,7 +368,7 @@ describe('TokenAccess', function() {
[
// no access before token is used
cb =>
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -273,15 +377,30 @@ describe('TokenAccess', function() {
},
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,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
},
cb
),
cb =>
tryContentAccess(
this.other1,
@@ -328,7 +447,7 @@ describe('TokenAccess', function() {
async.series(
[
cb =>
tryReadAccess(
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
@@ -344,6 +463,20 @@ describe('TokenAccess', function() {
(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 =>
@@ -377,7 +510,7 @@ describe('TokenAccess', function() {
async.series(
[
cb =>
tryReadAccess(
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
@@ -391,11 +524,25 @@ describe('TokenAccess', function() {
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,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
},
cb
),
// should not allow the user to join the project
cb =>
tryAnonContentAccess(
@@ -445,7 +592,7 @@ describe('TokenAccess', function() {
[
// deny access before the token is used
cb =>
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -462,6 +609,20 @@ describe('TokenAccess', function() {
(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('readAndWrite')
},
cb
),
cb =>
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
cb
),
cb =>
@@ -487,6 +648,140 @@ describe('TokenAccess', function() {
)
})
describe('upgrading from a read-only token', function() {
beforeEach(function(done) {
this.owner.createProject(
`token-rw-upgrade-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 user to access project via read-only, then upgrade to read-write', function(done) {
async.series(
[
// deny access before the token is used
cb =>
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(response.headers.location).to.match(/\/restricted.*/)
expect(body).to.match(/.*\/restricted.*/)
},
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(200)
expect(body.redirect).to.equal(`/project/${this.projectId}`)
expect(body.tokenAccessGranted).to.equal('readOnly')
},
cb
)
},
cb => {
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
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.project.owner).to.have.keys('_id')
expect(body.project.owner).to.not.have.any.keys(
'email',
'first_name',
'last_name'
)
},
cb
)
},
//
// Then switch to 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(body.redirect).to.equal(`/project/${this.projectId}`)
expect(body.tokenAccessGranted).to.equal('readAndWrite')
},
cb
),
cb =>
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
cb
),
cb =>
tryContentAccess(
this.other1,
this.projectId,
(response, body) => {
expect(body.privilegeLevel).to.equal('readAndWrite')
expect(body.isRestrictedUser).to.equal(false)
expect(body.project.owner).to.have.all.keys(
'_id',
'email',
'first_name',
'last_name',
'privileges',
'signUpDate'
)
},
cb
)
],
done
)
})
})
describe('made private again', function() {
beforeEach(function(done) {
this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000))
@@ -496,7 +791,7 @@ describe('TokenAccess', function() {
async.series(
[
cb => {
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -510,12 +805,26 @@ describe('TokenAccess', function() {
tryReadAndWriteTokenAccess(
this.other1,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
)
},
cb => {
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
},
cb
)
},
cb => {
tryContentAccess(
this.other1,
@@ -564,7 +873,7 @@ describe('TokenAccess', function() {
async.series(
[
cb =>
tryReadAccess(
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
@@ -578,8 +887,14 @@ describe('TokenAccess', function() {
this.anon,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
redirect: '/restricted',
anonWriteAccessDenied: true
})
},
cb
),
@@ -629,7 +944,7 @@ describe('TokenAccess', function() {
async.series(
[
cb =>
tryReadAccess(
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
@@ -645,6 +960,20 @@ describe('TokenAccess', function() {
(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('readAndWrite')
},
cb
),
cb =>
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
cb
),
cb =>
@@ -671,7 +1000,7 @@ describe('TokenAccess', function() {
async.series(
[
cb =>
tryReadAccess(
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
@@ -684,11 +1013,24 @@ describe('TokenAccess', function() {
tryReadAndWriteTokenAccess(
this.anon,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryEditorAccess(
this.anon,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
},
cb
),
cb =>
tryAnonContentAccess(
this.anon,
@@ -746,15 +1088,19 @@ describe('TokenAccess', function() {
this.owner,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(response.headers.location).to.equal(
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 =>
tryReadAccess(
tryEditorAccess(
this.owner,
this.projectId,
(response, body) => {
@@ -826,10 +1172,14 @@ describe('TokenAccess', function() {
this.other1,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(response.headers.location).to.equal(
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
),
@@ -839,16 +1189,20 @@ describe('TokenAccess', function() {
this.other1,
this.tokens.readOnly,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(response.headers.location).to.equal(
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 =>
tryReadAccess(
tryEditorAccess(
this.other1,
this.projectId,
(response, body) => {
@@ -867,6 +1221,16 @@ describe('TokenAccess', function() {
cb
),
// should not allow a different user to join the project
cb =>
tryEditorAccess(
this.other2,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(302)
expect(body).to.match(/.*\/restricted.*/)
},
cb
),
cb =>
tryContentAccess(
this.other2,
@@ -893,24 +1257,32 @@ describe('TokenAccess', function() {
})
it('should show error page for read and write token', function(done) {
const unimportedV1Token = '123abc'
const unimportedV1Token = '123abcdefabcdef'
tryReadAndWriteTokenAccess(
this.owner,
unimportedV1Token,
(response, body) => {
expect(response.statusCode).to.equal(400)
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({ v1Import: { status: 'cannotImport' } })
},
done
)
})
it('should show error page for read only token to v1', function(done) {
const unimportedV1Token = 'abcd'
const unimportedV1Token = 'aaaaaabbbbbb'
tryReadOnlyTokenAccess(
this.owner,
unimportedV1Token,
(response, body) => {
expect(response.statusCode).to.equal(400)
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({ v1Import: { status: 'cannotImport' } })
},
done
)
@@ -928,33 +1300,44 @@ describe('TokenAccess', function() {
return done(err)
}
this.projectId = projectId
this.owner.makeTokenBased(this.projectId, err => {
if (err != null) {
return done(err)
}
db.projects.update(
{ _id: ObjectId(projectId) },
{ $set: { overleaf: { id: 1234 } } },
err => {
db.users.update(
{ _id: ObjectId(this.owner._id.toString()) },
{ $set: { 'overleaf.id': 321321 } },
err => {
if (err) {
return done(err)
}
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)
db.projects.update(
{ _id: ObjectId(projectId) },
{ $set: { overleaf: { id: 1234 } } },
err => {
if (err != null) {
return done(err)
}
this.owner.getProject(this.projectId, (err, project) => {
if (err != null) {
return done(err)
}
this.tokens = project.tokens
const docInfo = {
exists: true,
exported: false,
has_owner: true,
name: 'Test Project Import Example'
}
MockV1Api.setDocInfo(this.tokens.readAndWrite, docInfo)
MockV1Api.setDocInfo(this.tokens.readOnly, docInfo)
db.projects.remove({ _id: ObjectId(projectId) }, done)
})
}
this.tokens = project.tokens
MockV1Api.setDocExported(this.tokens.readAndWrite, {
exporting: true
})
MockV1Api.setDocExported(this.tokens.readOnly, {
exporting: true
})
done()
})
}
)
})
)
})
}
)
}
)
})
@@ -973,7 +1356,17 @@ describe('TokenAccess', function() {
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.include('ImportingController')
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
status: 'canImport',
projectId: this.tokens.readAndWrite,
hasOwner: true,
name: 'Test Project Import Example'
}
})
},
cb
),
@@ -983,7 +1376,35 @@ describe('TokenAccess', function() {
this.tokens.readOnly,
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.include('ImportingController')
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
status: 'canImport',
projectId: this.tokens.readOnly,
hasOwner: true,
name: 'Test Project Import Example'
}
})
},
cb
),
cb =>
tryEditorAccess(
this.owner,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryContentAccess(
this.other2,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
)
@@ -992,19 +1413,60 @@ describe('TokenAccess', function() {
)
})
describe('when importing check not configured', function() {
beforeEach(function() {
delete settings.projectImportingCheckMaxCreateDelta
describe('when the v1 doc does not exist', function(done) {
beforeEach(function(done) {
const docInfo = null
MockV1Api.setDocInfo(this.tokens.readAndWrite, docInfo)
MockV1Api.setDocInfo(this.tokens.readOnly, docInfo)
done()
})
it('should load editor', function(done) {
tryReadAndWriteTokenAccess(
this.owner,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.include('IdeController')
},
it('should get a 404 response on the post endpoint', function(done) {
async.series(
[
cb =>
tryReadAndWriteTokenAccess(
this.owner,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryReadOnlyTokenAccess(
this.owner,
this.tokens.readOnly,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryEditorAccess(
this.owner,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryContentAccess(
this.other2,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
)
],
done
)
})

View File

@@ -33,6 +33,16 @@ module.exports = MockV1Api = {
return (this.users[id] = user)
},
docInfo: {},
getDocInfo(token) {
return this.docInfo[token] || null
},
setDocInfo(token, info) {
this.docInfo[token] = info
},
exportId: null,
exportParams: null,
@@ -241,10 +251,11 @@ module.exports = MockV1Api = {
app.get(
'/api/v1/sharelatex/users/:user_id/docs/:token/info',
(req, res, next) => {
return res.json({
exists: true,
const info = this.getDocInfo(req.params.token) || {
exists: false,
exported: false
})
}
return res.json(info)
}
)

View File

@@ -35,7 +35,9 @@ describe('AuthorizationManager', function() {
},
'../Errors/Errors': Errors,
'../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = {
isValidToken: sinon.stub().callsArgWith(2, null, false, false)
validateTokenForAnonymousAccess: sinon
.stub()
.callsArgWith(2, null, false, false)
}),
'settings-sharelatex': { passwordStrengthOptions: {} }
}
@@ -160,7 +162,7 @@ describe('AuthorizationManager', function() {
describe('with no user (anonymous)', function() {
describe('when the token is not valid', function() {
beforeEach(function() {
this.TokenAccessHandler.isValidToken = sinon
this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon
.stub()
.withArgs(this.project_id, this.token)
.yields(null, false, false)
@@ -185,7 +187,7 @@ describe('AuthorizationManager', function() {
})
it('should check if the token is valid', function() {
return this.TokenAccessHandler.isValidToken
return this.TokenAccessHandler.validateTokenForAnonymousAccess
.calledWith(this.project_id, this.token)
.should.equal(true)
})
@@ -198,90 +200,47 @@ describe('AuthorizationManager', function() {
})
describe('when the token is valid for read-and-write', function() {
describe('when read-write-sharing is not enabled', function() {
beforeEach(function() {
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
this.TokenAccessHandler.isValidToken = sinon
.stub()
.withArgs(this.project_id, this.token)
.yields(null, true, false)
return this.AuthorizationManager.getPrivilegeLevelForProject(
null,
this.project_id,
this.token,
this.callback
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() {
return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should not call AuthorizationManager.isUserSiteAdmin', function() {
return this.AuthorizationManager.isUserSiteAdmin.called.should.equal(
false
)
})
it('should check if the token is valid', function() {
return this.TokenAccessHandler.isValidToken
.calledWith(this.project_id, this.token)
.should.equal(true)
})
it('should deny access', function() {
return this.callback
.calledWith(null, false, false, false)
.should.equal(true)
})
beforeEach(function() {
this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon
.stub()
.withArgs(this.project_id, this.token)
.yields(null, true, false)
return this.AuthorizationManager.getPrivilegeLevelForProject(
null,
this.project_id,
this.token,
this.callback
)
})
describe('when read-write-sharing is enabled', function() {
beforeEach(function() {
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
this.TokenAccessHandler.isValidToken = sinon
.stub()
.withArgs(this.project_id, this.token)
.yields(null, true, false)
return this.AuthorizationManager.getPrivilegeLevelForProject(
null,
this.project_id,
this.token,
this.callback
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() {
return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() {
return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal(
false
)
})
it('should not call AuthorizationManager.isUserSiteAdmin', function() {
return this.AuthorizationManager.isUserSiteAdmin.called.should.equal(
false
)
})
it('should not call AuthorizationManager.isUserSiteAdmin', function() {
return this.AuthorizationManager.isUserSiteAdmin.called.should.equal(
false
)
})
it('should check if the token is valid', function() {
return this.TokenAccessHandler.validateTokenForAnonymousAccess
.calledWith(this.project_id, this.token)
.should.equal(true)
})
it('should check if the token is valid', function() {
return this.TokenAccessHandler.isValidToken
.calledWith(this.project_id, this.token)
.should.equal(true)
})
it('should give read-write access', function() {
return this.callback
.calledWith(null, 'readAndWrite', false)
.should.equal(true)
})
it('should give read-write access', function() {
return this.callback
.calledWith(null, 'readAndWrite', false)
.should.equal(true)
})
})
describe('when the token is valid for read-only', function() {
beforeEach(function() {
this.TokenAccessHandler.isValidToken = sinon
this.TokenAccessHandler.validateTokenForAnonymousAccess = sinon
.stub()
.withArgs(this.project_id, this.token)
.yields(null, false, true)
@@ -306,7 +265,7 @@ describe('AuthorizationManager', function() {
})
it('should check if the token is valid', function() {
return this.TokenAccessHandler.isValidToken
return this.TokenAccessHandler.validateTokenForAnonymousAccess
.calledWith(this.project_id, this.token)
.should.equal(true)
})

View File

@@ -1323,11 +1323,6 @@ describe('ProjectController', function() {
archived: false,
trashed: false,
owner_ref: 'defg',
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
})
})
@@ -1359,11 +1354,6 @@ describe('ProjectController', function() {
archived: true,
trashed: false,
owner_ref: 'defg',
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
})
})
@@ -1390,11 +1380,6 @@ describe('ProjectController', function() {
archived: false,
trashed: false,
owner_ref: null,
tokens: {
readAndWrite: '1abcd',
readAndWritePrefix: '1',
readOnly: 'neiotsranteoia'
},
isV1Project: false
})
})

View File

@@ -25,11 +25,12 @@ const { ObjectId } = require('mongojs')
describe('TokenAccessHandler', function() {
beforeEach(function() {
this.token = 'sometokenthing'
this.token = 'abcdefabcdef'
this.projectId = ObjectId()
this.project = {
_id: this.projectId,
publicAccesLevel: 'tokenBased'
publicAccesLevel: 'tokenBased',
owner_ref: ObjectId()
}
this.userId = ObjectId()
this.req = {}
@@ -44,414 +45,78 @@ describe('TokenAccessHandler', function() {
'../User/UserGetter': (this.UserGetter = {}),
'../V1/V1Api': (this.V1Api = {
request: sinon.stub()
})
}),
crypto: (this.Crypto = require('crypto'))
}
}))
})
describe('findProjectWithReadOnlyToken', function() {
beforeEach(function() {
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, null, this.project))
})
it('should call Project.findOne', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project) => {
expect(this.Project.findOne.callCount).to.equal(1)
expect(
this.Project.findOne.calledWith({
'tokens.readOnly': this.token
})
).to.equal(true)
return done()
}
)
})
it('should produce a project object with no error', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.exist
expect(project).to.deep.equal(this.project)
return done()
}
)
})
it('should return projectExists flag as true', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project, projectExists) => {
expect(projectExists).to.equal(true)
return done()
}
)
})
describe('when Project.findOne produces an error', function() {
beforeEach(function() {
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, new Error('woops')))
})
it('should produce an error', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project) => {
expect(err).to.exist
expect(project).to.not.exist
expect(err).to.be.instanceof(Error)
return done()
}
)
})
})
describe('when project does not have tokenBased access level', function() {
beforeEach(function() {
this.project.publicAccesLevel = 'private'
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, null, this.project, true))
})
it('should not return a project', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.not.exist
return done()
}
)
})
it('should return projectExists flag as true', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project, projectExists) => {
expect(projectExists).to.equal(true)
return done()
}
)
})
})
describe('when project does not exist', function() {
beforeEach(function() {
return (this.Project.findOne = sinon.stub().callsArgWith(2, null, null))
})
it('should not return a project', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.not.exist
return done()
}
)
})
it('should return projectExists flag as false', function(done) {
return this.TokenAccessHandler.findProjectWithReadOnlyToken(
this.token,
(err, project, projectExists) => {
expect(projectExists).to.equal(false)
return done()
}
)
})
})
})
describe('findProjectWithReadAndWriteToken', function() {
beforeEach(function() {
this.token = '1234bcdf'
this.tokenPrefix = '1234'
this.project.tokens = {
readOnly: 'atntntn',
readAndWrite: this.token,
readAndWritePrefix: this.tokenPrefix
describe('getTokenType', function() {
it('should determine tokens correctly', function() {
const specs = {
abcdefabcdef: 'readOnly',
aaaaaabbbbbb: 'readOnly',
'54325aaaaaa': 'readAndWrite',
'54325aaaaaabbbbbb': 'readAndWrite',
'': null,
abc123def: null
}
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, null, this.project))
for (var token of Object.keys(specs)) {
expect(this.TokenAccessHandler.getTokenType(token)).to.equal(
specs[token]
)
}
})
})
describe('getProjectByReadOnlyToken', function() {
beforeEach(function() {
this.token = 'abcdefabcdef'
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
})
it('should call Project.findOne', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project) => {
expect(this.Project.findOne.callCount).to.equal(1)
expect(
this.Project.findOne.calledWith({
'tokens.readAndWritePrefix': this.tokenPrefix
})
).to.equal(true)
return done()
}
)
})
it('should produce a project object with no error', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
it('should get the project', function(done) {
this.TokenAccessHandler.getProjectByReadOnlyToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.exist
expect(project).to.deep.equal(this.project)
return done()
expect(this.Project.findOne.callCount).to.equal(1)
done()
}
)
})
it('should return projectExists flag as true', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project, projectExists) => {
expect(projectExists).to.equal(true)
return done()
}
)
})
describe('when Project.findOne produces an error', function() {
beforeEach(function() {
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, new Error('woops')))
})
it('should produce an error', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project) => {
expect(err).to.exist
expect(project).to.not.exist
expect(err).to.be.instanceof(Error)
return done()
}
)
})
})
describe('when project does not have tokenBased access level', function() {
beforeEach(function() {
this.project.publicAccesLevel = 'private'
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, null, this.project, true))
})
it('should not return a project', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.not.exist
return done()
}
)
})
it('should return projectExists flag as true', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project, projectExists) => {
expect(projectExists).to.equal(true)
return done()
}
)
})
})
describe('when the tokens have different lengths', function() {
beforeEach(function() {
this.project.tokens = {
readOnly: 'atntntn',
readAndWrite: this.token + 'some-other-characters',
readAndWritePrefix: this.tokenPrefix
}
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, null, this.project))
})
it('should not return a project', function(done) {
return this.TokenAccessHandler.findProjectWithReadAndWriteToken(
this.token,
(err, project) => {
expect(err).to.not.exist
expect(project).to.not.exist
return done()
}
)
})
})
})
describe('findProjectWithHigherAccess', function() {
describe('when user does have higher access', function() {
beforeEach(function() {
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon
.stub()
.callsArgWith(2, null, true))
})
it('should call Project.findOne', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(this.Project.findOne.callCount).to.equal(1)
expect(
this.Project.findOne.calledWith({
'tokens.readOnly': this.token
})
).to.equal(true)
return done()
}
)
})
it('should call isUserInvitedMemberOfProject', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(
this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount
).to.equal(1)
expect(
this.CollaboratorsGetter.isUserInvitedMemberOfProject.calledWith(
this.userId,
this.project._id
)
).to.equal(true)
return done()
}
)
})
it('should produce a project object', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(err).to.not.exist
expect(project).to.exist
expect(project).to.deep.equal(this.project)
return done()
}
)
})
describe('getProjectByReadAndWriteToken', function() {
beforeEach(function() {
sinon.spy(this.Crypto, 'timingSafeEqual')
this.token = '1234abcdefabcdef'
this.project.tokens = {
readAndWrite: this.token,
readAndWritePrefix: '1234'
}
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
})
describe('when user does not have higher access', function() {
beforeEach(function() {
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon
.stub()
.callsArgWith(2, null, false))
})
it('should call Project.findOne', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(this.Project.findOne.callCount).to.equal(1)
expect(
this.Project.findOne.calledWith({
'tokens.readOnly': this.token
})
).to.equal(true)
return done()
}
)
})
it('should call isUserInvitedMemberOfProject', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(
this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount
).to.equal(1)
expect(
this.CollaboratorsGetter.isUserInvitedMemberOfProject.calledWith(
this.userId,
this.project._id
)
).to.equal(true)
return done()
}
)
})
it('should not produce a project', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(err).to.not.exist
expect(project).to.not.exist
return done()
}
)
})
afterEach(function() {
this.Crypto.timingSafeEqual.restore()
})
describe('when Project.findOne produces an error', function() {
beforeEach(function() {
return (this.Project.findOne = sinon
.stub()
.callsArgWith(2, new Error('woops')))
})
it('should produce an error', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(err).to.exist
expect(project).to.not.exist
expect(err).to.be.instanceof(Error)
return done()
}
)
})
})
describe('when isUserInvitedMemberOfProject produces an error', function() {
beforeEach(function() {
this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon
.stub()
.callsArgWith(2, new Error('woops')))
})
it('should produce an error', function(done) {
return this.TokenAccessHandler.findProjectWithHigherAccess(
this.token,
this.userId,
(err, project) => {
expect(err).to.exist
expect(project).to.not.exist
expect(err).to.be.instanceof(Error)
return done()
}
)
})
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()
}
)
})
})
@@ -583,27 +248,22 @@ describe('TokenAccessHandler', function() {
})
})
describe('isValidToken', function() {
describe('validateTokenForAnonymousAccess', function() {
describe('when a read-only project is found', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.stub()
.callsArgWith(1, null, this.project))
.callsArgWith(2, null, this.project)
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
this.TokenAccessHandler.getProjectByToken.callCount
).to.equal(1)
return done()
}
@@ -611,7 +271,7 @@ describe('TokenAccessHandler', function() {
})
it('should allow read-only access', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
@@ -626,64 +286,93 @@ describe('TokenAccessHandler', function() {
describe('when a read-and-write project is found', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getTokenType = sinon
.stub()
.callsArgWith(1, null, this.project)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.returns('readAndWrite')
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null))
.callsArgWith(2, null, this.project)
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
).to.equal(1)
return done()
}
)
describe('when Anonymous token access is not enabled', function(done) {
beforeEach(function() {
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
expect(
this.TokenAccessHandler.getProjectByToken.callCount
).to.equal(1)
return done()
}
)
})
it('should not allow read-and-write access', function(done) {
return 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)
return done()
}
)
})
})
it('should allow read-and-write access', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
this.token,
(err, rw, ro) => {
expect(err).to.not.exist
expect(rw).to.equal(true)
expect(ro).to.equal(false)
return done()
}
)
describe('when anonymous token access is enabled', function(done) {
beforeEach(function() {
this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
expect(
this.TokenAccessHandler.getProjectByToken.callCount
).to.equal(1)
return done()
}
)
})
it('should allow read-and-write access', function(done) {
return 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)
return done()
}
)
})
})
})
describe('when no project is found', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.stub()
.callsArgWith(1, null, null))
.callsArgWith(2, null, null, null)
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
this.TokenAccessHandler.getProjectByToken.callCount
).to.equal(1)
return done()
}
@@ -691,7 +380,7 @@ describe('TokenAccessHandler', function() {
})
it('should not allow any access', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
@@ -706,24 +395,18 @@ describe('TokenAccessHandler', function() {
describe('when findProject produces an error', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.stub()
.callsArgWith(1, new Error('woops')))
.callsArgWith(2, new Error('woops'))
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
this.TokenAccessHandler.getProjectByToken.callCount
).to.equal(1)
return done()
}
@@ -731,7 +414,7 @@ describe('TokenAccessHandler', function() {
})
it('should produce an error and not allow access', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
@@ -752,33 +435,16 @@ describe('TokenAccessHandler', function() {
describe('for read-and-write project', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getTokenType = sinon
.stub()
.callsArgWith(1, null, this.project)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.returns('readAndWrite')
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null))
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken
.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
).to.equal(1)
return done()
}
)
.callsArgWith(2, null, this.project)
})
it('should not allow any access', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
@@ -793,33 +459,16 @@ describe('TokenAccessHandler', function() {
describe('for read-only project', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
this.TokenAccessHandler.getTokenType = sinon
.stub()
.callsArgWith(1, null, null)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.returns('readOnly')
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, this.project))
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
this.token,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken
.callCount
).to.equal(1)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
).to.equal(1)
return done()
}
)
.callsArgWith(2, null, this.project)
})
it('should not allow any access', function(done) {
return this.TokenAccessHandler.isValidToken(
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
this.token,
(err, rw, ro) => {
@@ -831,58 +480,26 @@ describe('TokenAccessHandler', function() {
)
})
})
})
describe('with nothing', function() {
beforeEach(function() {
this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon
.stub()
.callsArgWith(1, null, this.project)
return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon
.stub()
.callsArgWith(1, null, null))
})
describe('with nothing', function() {
beforeEach(function() {
this.TokenAccessHandler.getProjectByToken = sinon
.stub()
.callsArgWith(1, null, null, null)
})
it('should not call findProjectWithReadOnlyToken', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
null,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
).to.equal(0)
return done()
}
)
})
it('should try to find projects with both kinds of token', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
null,
(err, allowed) => {
expect(
this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount
).to.equal(0)
expect(
this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount
).to.equal(0)
return done()
}
)
})
it('should not allow any access', function(done) {
return this.TokenAccessHandler.isValidToken(
this.projectId,
null,
(err, rw, ro) => {
expect(err).to.not.exist
expect(rw).to.equal(false)
expect(ro).to.equal(false)
return done()
}
)
it('should not allow any access', function(done) {
return this.TokenAccessHandler.validateTokenForAnonymousAccess(
this.projectId,
null,
(err, rw, ro) => {
expect(err).to.not.exist
expect(rw).to.equal(false)
expect(ro).to.equal(false)
return done()
}
)
})
})
})
})