Add a setting to enable anonymous read-and-write link sharing

This commit is contained in:
Shane Kilkelly 2017-10-18 13:04:37 +01:00
parent 9c247d5f59
commit 7d2bde85ff
9 changed files with 253 additions and 81 deletions

View file

@ -35,15 +35,23 @@ module.exports = AuthorizationManager =
return callback(err) if err? return callback(err) if err?
if publicAccessLevel == PublicAccessLevels.TOKEN_BASED if publicAccessLevel == PublicAccessLevels.TOKEN_BASED
# Anonymous users can have read-only access to token-based projects, # Anonymous users can have read-only access to token-based projects,
# while read-write access must be logged in # while read-write access must be logged in,
TokenAccessHandler.isValidReadOnlyToken project_id, token, (err, isValid) -> # unless the `enableAnonymousReadAndWriteSharing` setting is enabled
return callback(err) if err? TokenAccessHandler.isValidToken project_id, token,
if isValid (err, isValidReadAndWrite, isValidReadOnly) ->
# Grant anonymous user read-only access return callback(err) if err?
callback null, PrivilegeLevels.READ_ONLY, false if isValidReadOnly
else # Grant anonymous user read-only access
# Deny anonymous user access callback null, PrivilegeLevels.READ_ONLY, false
callback null, PrivilegeLevels.NONE, false else if (
isValidReadAndWrite and
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
)
# Grant anonymous user read-and-write access
callback null, PrivilegeLevels.READ_AND_WRITE, false
else
# Deny anonymous access
callback null, PrivilegeLevels.NONE, false
else if publicAccessLevel == PublicAccessLevels.READ_ONLY else if publicAccessLevel == PublicAccessLevels.READ_ONLY
# Legacy public read-only access for anonymous user # Legacy public read-only access for anonymous user
callback null, PrivilegeLevels.READ_ONLY, true callback null, PrivilegeLevels.READ_ONLY, true

View file

@ -4,7 +4,6 @@ TokenAccessHandler = require './TokenAccessHandler'
Errors = require '../Errors/Errors' Errors = require '../Errors/Errors'
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
module.exports = TokenAccessController = module.exports = TokenAccessController =
_loadEditor: (projectId, req, res, next) -> _loadEditor: (projectId, req, res, next) ->
@ -23,6 +22,10 @@ module.exports = TokenAccessController =
if !project? if !project?
logger.log {token, userId}, logger.log {token, userId},
"no project found for readAndWrite token" "no project found for readAndWrite token"
if !userId?
logger.log {token},
"No project found with read-write token, anonymous user"
return next(new Errors.NotFoundError())
TokenAccessHandler TokenAccessHandler
.findPrivateOverleafProjectWithReadAndWriteToken token, (err, project) -> .findPrivateOverleafProjectWithReadAndWriteToken token, (err, project) ->
if err? if err?
@ -36,6 +39,17 @@ module.exports = TokenAccessController =
logger.log {token, projectId: project._id}, "redirecting user to project" logger.log {token, projectId: project._id}, "redirecting user to project"
res.redirect(302, "/project/#{project._id}") res.redirect(302, "/project/#{project._id}")
else else
if !userId?
if TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
logger.log {token, projectId: project._id},
"allow anonymous read-and-write token access"
TokenAccessHandler.grantSessionTokenAccess(req, project._id, token)
req._anonToken = token
return TokenAccessController._loadEditor(project._id, req, res, next)
else
logger.log {token, projectId: project._id},
"deny anonymous read-and-write token access"
return next(new Errors.NotFoundError())
if project.owner_ref.toString() == userId if project.owner_ref.toString() == userId
logger.log {userId, projectId: project._id}, logger.log {userId, projectId: project._id},
"user is already project owner" "user is already project owner"
@ -65,7 +79,7 @@ module.exports = TokenAccessController =
if !userId? if !userId?
logger.log {userId, projectId: project._id}, logger.log {userId, projectId: project._id},
"adding anonymous user to project with readOnly token" "adding anonymous user to project with readOnly token"
TokenAccessHandler.grantSessionReadOnlyTokenAccess(req, project._id, token) TokenAccessHandler.grantSessionTokenAccess(req, project._id, token)
req._anonToken = token req._anonToken = token
return TokenAccessController._loadEditor(project._id, req, res, next) return TokenAccessController._loadEditor(project._id, req, res, next)
else else
@ -80,7 +94,6 @@ module.exports = TokenAccessController =
logger.err {err, token, userId, projectId: project._id}, logger.err {err, token, userId, projectId: project._id},
"error adding user to project with readAndWrite token" "error adding user to project with readAndWrite token"
return next(err) return next(err)
req.params.Project_id = project._id.toString() return TokenAccessController._loadEditor(project._id, req, res, next)
return ProjectController.loadEditor(req, res, next)

View file

@ -1,9 +1,13 @@
Project = require('../../models/Project').Project Project = require('../../models/Project').Project
PublicAccessLevels = require '../Authorization/PublicAccessLevels' PublicAccessLevels = require '../Authorization/PublicAccessLevels'
ObjectId = require("mongojs").ObjectId ObjectId = require("mongojs").ObjectId
Settings = require('settings-sharelatex')
module.exports = TokenAccessHandler = module.exports = TokenAccessHandler =
ANONYMOUS_READ_AND_WRITE_ENABLED:
Settings.allowAnonymousReadAndWriteSharing == true
findProjectWithReadOnlyToken: (token, callback=(err, project)->) -> findProjectWithReadOnlyToken: (token, callback=(err, project)->) ->
Project.findOne { Project.findOne {
'tokens.readOnly': token, 'tokens.readOnly': token,
@ -41,28 +45,30 @@ module.exports = TokenAccessHandler =
$addToSet: {tokenAccessReadAndWrite_refs: userId} $addToSet: {tokenAccessReadAndWrite_refs: userId}
}, callback }, callback
grantSessionReadOnlyTokenAccess: (req, projectId, token) -> grantSessionTokenAccess: (req, projectId, token) ->
if req.session? if req.session?
if !req.session.anonReadOnlyTokenAccess? if !req.session.anonTokenAccess?
req.session.anonReadOnlyTokenAccess = {} req.session.anonTokenAccess = {}
req.session.anonReadOnlyTokenAccess[projectId.toString()] = token.toString() req.session.anonTokenAccess[projectId.toString()] = token.toString()
getRequestToken: (req, projectId) -> getRequestToken: (req, projectId) ->
token = ( token = (
req?.session?.anonReadOnlyTokenAccess?[projectId.toString()] or req?.session?.anonTokenAccess?[projectId.toString()] or
req?.headers['x-sl-anon-token'] req?.headers['x-sl-anon-token']
) )
return token return token
isValidReadOnlyToken: (projectId, token, callback=(err, allowed)->) -> isValidToken: (projectId, token, callback=(err, isValidReadAndWrite, isValidReadOnly)->) ->
if !token if !token
return callback null, false return callback null, false, false
TokenAccessHandler.findProjectWithReadOnlyToken token, (err, project) -> _validate = (project) ->
project? and
project.publicAccesLevel == PublicAccessLevels.TOKEN_BASED and
project._id.toString() == projectId.toString()
TokenAccessHandler.findProjectWithReadAndWriteToken token, (err, readAndWriteProject) ->
return callback(err) if err? return callback(err) if err?
isAllowed = ( isValidReadAndWrite = _validate(readAndWriteProject)
project? and TokenAccessHandler.findProjectWithReadOnlyToken token, (err, readOnlyProject) ->
project.publicAccesLevel == PublicAccessLevels.TOKEN_BASED and return callback(err) if err?
project._id.toString() == projectId.toString() isValidReadOnly = _validate(readOnlyProject)
) callback null, isValidReadAndWrite, isValidReadOnly
callback null, isAllowed

View file

@ -343,7 +343,6 @@ module.exports = class Router
TokenAccessController.readOnlyToken TokenAccessController.readOnlyToken
webRouter.get '/:read_and_write_token([0-9]+[a-z]+)', webRouter.get '/:read_and_write_token([0-9]+[a-z]+)',
AuthenticationController.requireLogin(),
TokenAccessController.readAndWriteToken TokenAccessController.readAndWriteToken
webRouter.get '*', ErrorController.notFound webRouter.get '*', ErrorController.notFound

View file

@ -16,6 +16,11 @@ httpAuthUsers[httpAuthUser] = httpAuthPass
sessionSecret = "secret-please-change" sessionSecret = "secret-please-change"
module.exports = settings = module.exports = settings =
allowAnonymousReadAndWriteSharing:
process.env['SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING'] == 'true'
# File storage # File storage
# ------------ # ------------
# #

View file

@ -14,7 +14,7 @@ describe "AuthorizationManager", ->
"../../models/User": User: @User = {} "../../models/User": User: @User = {}
"../Errors/Errors": Errors "../Errors/Errors": Errors
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler = { "../TokenAccess/TokenAccessHandler": @TokenAccessHandler = {
isValidReadOnlyToken: sinon.stub().callsArgWith(2, null, false) isValidToken: sinon.stub().callsArgWith(2, null, false, false)
} }
@user_id = "user-id-1" @user_id = "user-id-1"
@project_id = "project-id-1" @project_id = "project-id-1"

View file

@ -463,7 +463,7 @@ describe "TokenAccessController", ->
describe 'anonymous', -> describe 'anonymous', ->
beforeEach -> beforeEach ->
@AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null)
@TokenAccessHandler.grantSessionReadOnlyTokenAccess = sinon.stub() @TokenAccessHandler.grantSessionTokenAccess = sinon.stub()
describe 'when all goes well', -> describe 'when all goes well', ->
beforeEach -> beforeEach ->
@ -486,9 +486,9 @@ describe "TokenAccessController", ->
done() done()
it 'should give the user session read-only access', (done) -> it 'should give the user session read-only access', (done) ->
expect(@TokenAccessHandler.grantSessionReadOnlyTokenAccess.callCount) expect(@TokenAccessHandler.grantSessionTokenAccess.callCount)
.to.equal 1 .to.equal 1
expect(@TokenAccessHandler.grantSessionReadOnlyTokenAccess.calledWith( expect(@TokenAccessHandler.grantSessionTokenAccess.calledWith(
@req, @projectId, @readOnlyToken @req, @projectId, @readOnlyToken
)) ))
.to.equal true .to.equal true
@ -527,7 +527,7 @@ describe "TokenAccessController", ->
done() done()
it 'should not give the user session read-only access', (done) -> it 'should not give the user session read-only access', (done) ->
expect(@TokenAccessHandler.grantSessionReadOnlyTokenAccess.callCount) expect(@TokenAccessHandler.grantSessionTokenAccess.callCount)
.to.equal 0 .to.equal 0
done() done()
@ -567,7 +567,7 @@ describe "TokenAccessController", ->
done() done()
it 'should not give the user session read-only access', (done) -> it 'should not give the user session read-only access', (done) ->
expect(@TokenAccessHandler.grantSessionReadOnlyTokenAccess.callCount) expect(@TokenAccessHandler.grantSessionTokenAccess.callCount)
.to.equal 0 .to.equal 0
done() done()

View file

@ -19,6 +19,7 @@ describe "TokenAccessHandler", ->
@req = {} @req = {}
@TokenAccessHandler = SandboxedModule.require modulePath, requires: @TokenAccessHandler = SandboxedModule.require modulePath, requires:
'../../models/Project': {Project: @Project = {}} '../../models/Project': {Project: @Project = {}}
'settings-sharelatex': {}
describe 'findProjectWithReadOnlyToken', -> describe 'findProjectWithReadOnlyToken', ->
@ -175,98 +176,187 @@ describe "TokenAccessHandler", ->
done() done()
describe 'grantSessionReadOnlyTokenAccess', -> describe 'grantSessionTokenAccess', ->
beforeEach -> beforeEach ->
@req = {session: {}, headers: {}} @req = {session: {}, headers: {}}
it 'should add the token to the session', (done) -> it 'should add the token to the session', (done) ->
@TokenAccessHandler.grantSessionReadOnlyTokenAccess(@req, @projectId, @token) @TokenAccessHandler.grantSessionTokenAccess(@req, @projectId, @token)
expect(@req.session.anonReadOnlyTokenAccess[@projectId.toString()]) expect(@req.session.anonTokenAccess[@projectId.toString()])
.to.equal @token .to.equal @token
done() done()
describe 'isValidReadOnlyToken', ->
beforeEach ->
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, @project)
it 'should call findProjectWithReadOnlyToken', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1
done()
it 'should allow access', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) =>
expect(err).to.not.exist
expect(allowed).to.equal true
done()
describe 'when no project is found', ->
describe 'isValidToken', ->
describe 'when a read-only project is found', ->
beforeEach -> beforeEach ->
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, null) .callsArgWith(1, null, null)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, @project)
it 'should call findProjectWithReadOnlyToken', (done) -> it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
.to.equal 1
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1 .to.equal 1
done() done()
it 'should not allow access', (done) -> it 'should allow read-only access', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @req, @projectId, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.not.exist expect(err).to.not.exist
expect(allowed).to.equal false expect(rw).to.equal false
expect(ro).to.equal true
done() done()
describe 'when no findProject produces an error', -> describe 'when a read-and-write project is found', ->
beforeEach -> beforeEach ->
@TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, @project)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, null)
it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
.to.equal 1
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1
done()
it 'should allow read-and-write access', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.not.exist
expect(rw).to.equal true
expect(ro).to.equal false
done()
describe 'when no project is found', ->
beforeEach ->
@TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, null)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, null)
it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
.to.equal 1
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1
done()
it 'should not allow any access', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.not.exist
expect(rw).to.equal false
expect(ro).to.equal false
done()
describe 'when findProject produces an error', ->
beforeEach ->
@TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, null)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, new Error('woops')) .callsArgWith(1, new Error('woops'))
it 'should call findProjectWithReadOnlyToken', (done) -> it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
.to.equal 1
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1 .to.equal 1
done() done()
it 'should produce an error and not allow access', (done) -> it 'should produce an error and not allow access', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.exist expect(err).to.exist
expect(err).to.be.instanceof Error expect(err).to.be.instanceof Error
expect(allowed).to.equal undefined expect(rw).to.equal undefined
expect(ro).to.equal undefined
done() done()
describe 'when project is not set to token-based access', -> describe 'when project is not set to token-based access', ->
beforeEach -> beforeEach ->
@project.publicAccesLevel = 'private' @project.publicAccesLevel = 'private'
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, @project)
it 'should call findProjectWithReadOnlyToken', (done) -> describe 'for read-and-write project', ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) => beforeEach ->
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.to.equal 1 .callsArgWith(1, null, @project)
done() @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, null)
it 'should not allow access', (done) -> it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, @token, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(err).to.not.exist expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
expect(allowed).to.equal false .to.equal 1
done() expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1
done()
it 'should not allow any access', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.not.exist
expect(rw).to.equal false
expect(ro).to.equal false
done()
describe 'for read-only project', ->
beforeEach ->
@TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, null)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, @project)
it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
.to.equal 1
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 1
done()
it 'should not allow any access', (done) ->
@TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) =>
expect(err).to.not.exist
expect(rw).to.equal false
expect(ro).to.equal false
done()
describe 'with nothing', -> describe 'with nothing', ->
beforeEach -> beforeEach ->
@TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub()
.callsArgWith(1, null, @project)
@TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub()
.callsArgWith(1, null, null)
it 'should not call findProjectWithReadOnlyToken', (done) -> it 'should not call findProjectWithReadOnlyToken', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @projectId, null, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, null, (err, allowed) =>
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 0 .to.equal 0
done() done()
it 'should not allow access', (done) -> it 'should try to find projects with both kinds of token', (done) ->
@TokenAccessHandler.isValidReadOnlyToken @req, @projectId, (err, allowed) => @TokenAccessHandler.isValidToken @projectId, null, (err, allowed) =>
expect(err).to.not.exist expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount)
expect(allowed).to.equal false .to.equal 0
expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount)
.to.equal 0
done()
it 'should not allow any access', (done) ->
@TokenAccessHandler.isValidToken @projectId, null, (err, rw, ro) =>
expect(err).to.not.exist
expect(rw).to.equal false
expect(ro).to.equal false
done() done()

View file

@ -241,6 +241,57 @@ describe 'TokenAccess', ->
expect(body.privilegeLevel).to.equal false expect(body.privilegeLevel).to.equal false
, done) , done)
if !settings.allowAnonymousReadAndWriteSharing
console.log ">> Skipping anonymous read-write token tests"
else
describe 'anonymous read-and-write token', ->
before (done) ->
@owner.createProject 'token-anon-rw-test#{Math.random()}', (err, project_id) =>
return done(err) if err?
@project_id = project_id
@owner.makeTokenBased @project_id, (err) =>
return done(err) if err?
@owner.getProject @project_id, (err, project) =>
return done(err) if err?
@tokens = project.tokens
done()
it 'should deny access before the token is used', (done) ->
try_read_access(@anon, @project_id, (response, body) =>
expect(response.statusCode).to.equal 302
expect(body).to.match /.*\/restricted.*/
, done)
it 'should allow the user to access project via read-and-write token url', (done) ->
try_read_and_write_token_access(@anon, @tokens.readAndWrite, (response, body) =>
expect(response.statusCode).to.equal 200
, done)
it 'should allow the user to anonymously join the project with read-and-write access', (done) ->
try_anon_content_access(@anon, @project_id, @tokens.readAndWrite, (response, body) =>
expect(body.privilegeLevel).to.equal 'readAndWrite'
, done)
describe 'made private again', ->
before (done) ->
@owner.makePrivate @project_id, () -> setTimeout(done, 1000)
it 'should deny access to project', (done) ->
try_read_access(@anon, @project_id, (response, body) =>
expect(response.statusCode).to.equal 302
expect(body).to.match /.*\/restricted.*/
, done)
it 'should not allow the user to access read-and-write token', (done) ->
try_read_and_write_token_access(@anon, @tokens.readAndWrite, (response, body) =>
expect(response.statusCode).to.equal 404
, done)
it 'should not allow the user to join the project', (done) ->
try_anon_content_access(@anon, @project_id, @tokens.readAndWrite, (response, body) =>
expect(body.privilegeLevel).to.equal false
, done)
describe 'private overleaf project', -> describe 'private overleaf project', ->
before (done) -> before (done) ->