Merge branch 'master' into pr-bulk-actions

This commit is contained in:
Paulo Reis 2017-05-18 14:38:16 +01:00
commit e532f543a3
20 changed files with 931 additions and 63 deletions

View file

@ -196,7 +196,11 @@ module.exports = AuthenticationController =
_setRedirectInSession: (req, value) ->
if !value?
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
if (
req.session? &&
!value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$')) &&
!value.match(new RegExp('^.*\.(png|jpeg|svg)$'))
)
req.session.postLoginRedirect = value
_getRedirectFromSession: (req) ->

View file

@ -2,6 +2,8 @@ Settings = require "settings-sharelatex"
request = require('request')
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("clsi_cookie")
if Settings.redis.clsi_cookie_secondary?
rclient_secondary = RedisWrapper.client("clsi_cookie_secondary")
Cookie = require('cookie')
logger = require "logger-sharelatex"
@ -17,10 +19,10 @@ module.exports = ClsiCookieManager =
rclient.get buildKey(project_id), (err, serverId)->
if err?
return callback(err)
if serverId?
return callback(null, serverId)
else
if !serverId? or serverId == ""
return ClsiCookieManager._populateServerIdViaRequest project_id, callback
else
return callback(null, serverId)
_populateServerIdViaRequest :(project_id, callback = (err, serverId)->)->
@ -42,11 +44,16 @@ module.exports = ClsiCookieManager =
if !clsiCookiesEnabled
return callback()
serverId = ClsiCookieManager._parseServerIdFromResponse(response)
if rclient_secondary?
@_setServerIdInRedis rclient_secondary, project_id, serverId
@_setServerIdInRedis rclient, project_id, serverId, (err) ->
callback(err, serverId)
_setServerIdInRedis: (rclient, project_id, serverId, callback = (err) ->) ->
multi = rclient.multi()
multi.set buildKey(project_id), serverId
multi.expire buildKey(project_id), Settings.clsiCookie.ttl
multi.exec (err)->
callback(err, serverId)
multi.exec callback
clearServerId: (project_id, callback = (err)->)->
if !clsiCookiesEnabled

View file

@ -20,30 +20,33 @@ module.exports = FileStoreHandler =
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining"
return callback(new Error("can not upload symlink"))
_cb = callback
callback = (err) ->
callback = -> # avoid double callbacks
_cb(err)
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
readStream = fs.createReadStream(fsPath)
opts =
method: "post"
uri: FileStoreHandler._buildUrl(project_id, file_id)
timeout:fiveMinsInMs
writeStream = request(opts)
readStream.pipe writeStream
writeStream.on 'response', (response) ->
if response.statusCode not in [200, 201]
err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
callback(err)
else
callback(null)
readStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
callback err
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
readStream.on "open", () ->
opts =
method: "post"
uri: FileStoreHandler._buildUrl(project_id, file_id)
timeout:fiveMinsInMs
writeStream = request(opts)
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
writeStream.on 'response', (response) ->
if response.statusCode not in [200, 201]
err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
callback(err)
else
callback(null)
readStream.pipe writeStream
getFileStream: (project_id, file_id, query, callback)->
logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store"

View file

@ -0,0 +1,63 @@
logger = require 'logger-sharelatex'
SudoModeHandler = require './SudoModeHandler'
AuthenticationController = require '../Authentication/AuthenticationController'
AuthenticationManager = require '../Authentication/AuthenticationManager'
ObjectId = require('../../infrastructure/Mongoose').mongo.ObjectId
UserGetter = require '../User/UserGetter'
module.exports = SudoModeController =
sudoModePrompt: (req, res, next) ->
if req.externalAuthenticationSystemUsed()
logger.log {userId}, "[SudoMode] using external auth, redirecting"
return res.redirect('/project')
userId = AuthenticationController.getLoggedInUserId(req)
logger.log {userId}, "[SudoMode] rendering sudo mode password page"
SudoModeHandler.isSudoModeActive userId, (err, isActive) ->
if err?
logger.err {err, userId}, "[SudoMode] error checking if sudo mode is active"
return next(err)
if isActive
logger.log {userId}, "[SudoMode] sudo mode already active, redirecting"
return res.redirect('/project')
res.render 'sudo_mode/sudo_mode_prompt', title: 'confirm_password_to_continue'
submitPassword: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
password = req.body.password
if !password
logger.log {userId}, "[SudoMode] no password supplied, failed authentication"
return next(new Error('no password supplied'))
logger.log {userId, redir}, "[SudoMode] checking user password"
UserGetter.getUser ObjectId(userId), {email: 1}, (err, userRecord) ->
if err?
logger.err {err, userId}, "[SudoMode] error getting user"
return next(err)
if !userRecord?
err = new Error('user not found')
logger.err {err, userId}, "[SudoMode] user not found"
return next(err)
AuthenticationManager.authenticate email: userRecord.email, password, (err, user) ->
if err?
logger.err {err, userId}, "[SudoMode] error authenticating user"
return next(err)
if user?
logger.log {userId}, "[SudoMode] authenticated user, activating sudo mode"
SudoModeHandler.activateSudoMode userId, (err) ->
if err?
logger.err {err, userId}, "[SudoMode] error activating sudo mode"
return next(err)
return res.json {
redir: redir
}
else
logger.log {userId}, "[SudoMode] authentication failed for user"
return res.json {
message: {
text: req.i18n.translate("invalid_password"),
type: 'error'
}
}

View file

@ -0,0 +1,33 @@
RedisWrapper = require('../../infrastructure/RedisWrapper')
rclient = RedisWrapper.client('sudomode')
logger = require('logger-sharelatex')
TIMEOUT_IN_SECONDS = 60 * 60
module.exports = SudoModeHandler =
_buildKey: (userId) ->
"SudoMode:{#{userId}}"
activateSudoMode: (userId, callback=(err)->) ->
if !userId?
return callback(new Error('[SudoMode] user must be supplied'))
duration = TIMEOUT_IN_SECONDS
logger.log {userId, duration}, "[SudoMode] activating sudo mode for user"
rclient.set SudoModeHandler._buildKey(userId), '1', 'EX', duration, callback
clearSudoMode: (userId, callback=(err)->) ->
if !userId?
return callback(new Error('[SudoMode] user must be supplied'))
logger.log {userId}, "[SudoMode] clearing sudo mode for user"
rclient.del SudoModeHandler._buildKey(userId), callback
isSudoModeActive: (userId, callback=(err, isActive)->) ->
if !userId?
return callback(new Error('[SudoMode] user must be supplied'))
rclient.get SudoModeHandler._buildKey(userId), (err, result) ->
if err?
return callback(err)
callback(null, result == '1')

View file

@ -0,0 +1,24 @@
logger = require 'logger-sharelatex'
SudoModeHandler = require './SudoModeHandler'
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = SudoModeMiddlewear =
protectPage: (req, res, next) ->
if req.externalAuthenticationSystemUsed()
logger.log {userId}, "[SudoMode] using external auth, skipping sudo-mode check"
return next()
userId = AuthenticationController.getLoggedInUserId(req)
logger.log {userId}, "[SudoMode] protecting endpoint, checking if sudo mode is active"
SudoModeHandler.isSudoModeActive userId, (err, isActive) ->
if err?
logger.err {err, userId}, "[SudoMode] error checking if sudo mode is active"
return next(err)
if isActive
logger.log {userId}, "[SudoMode] sudo mode active, continuing"
return next()
else
logger.log {userId}, "[SudoMode] sudo mode not active, redirecting"
AuthenticationController._setRedirectInSession(req)
return res.redirect('/confirm-password')

View file

@ -11,6 +11,7 @@ AuthenticationManager = require("../Authentication/AuthenticationManager")
AuthenticationController = require('../Authentication/AuthenticationController')
UserSessionsManager = require("./UserSessionsManager")
UserUpdater = require("./UserUpdater")
SudoModeHandler = require('../SudoMode/SudoModeHandler')
settings = require "settings-sharelatex"
module.exports = UserController =
@ -118,6 +119,7 @@ module.exports = UserController =
if err
logger.err err: err, 'error destorying session'
UserSessionsManager.untrackSession(user, sessionId)
SudoModeHandler.clearSudoMode(user._id)
res.redirect '/login'
register : (req, res, next = (error) ->)->

View file

@ -84,6 +84,11 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use addSetContentDisposition
apiRouter.use addSetContentDisposition
webRouter.use (req, res, next)->
req.externalAuthenticationSystemUsed = res.locals.externalAuthenticationSystemUsed = ->
Settings.ldap? or Settings.saml?
next()
webRouter.use (req, res, next)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
@ -222,11 +227,6 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
webRouter.use (req, res, next)->
res.locals.externalAuthenticationSystemUsed = ->
Settings.ldap? or Settings.saml?
next()
webRouter.use (req, res, next)->
currentUser = AuthenticationController.getSessionUser(req)
if currentUser?

View file

@ -39,6 +39,8 @@ ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
SudoModeController = require('./Features/SudoMode/SudoModeController')
SudoModeMiddlewear = require('./Features/SudoMode/SudoModeMiddlewear')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
@ -86,11 +88,17 @@ module.exports = class Router
webRouter.get '/user/activate', UserPagesController.activateAccountPage
AuthenticationController.addEndpointToLoginWhitelist '/user/activate'
webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
webRouter.get '/user/settings',
AuthenticationController.requireLogin(),
SudoModeMiddlewear.protectPage,
UserPagesController.settingsPage
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage
webRouter.get '/user/sessions',
AuthenticationController.requireLogin(),
SudoModeMiddlewear.protectPage,
UserPagesController.sessionsPage
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
@ -239,6 +247,9 @@ module.exports = class Router
webRouter.get "/beta/participate", AuthenticationController.requireLogin(), BetaProgramController.optInPage
webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
webRouter.get "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.sudoModePrompt
webRouter.post "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.submitPassword
#Admin Stuff
webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index

View file

@ -0,0 +1,45 @@
extends ../layout
block content
.content.content-alt
.container
.row
.col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3
.card
.page-header.text-centered
h2 #{translate('confirm_password_to_continue')}
div
.container-fluid
.row
.col-md-12
form(async-form="confirmPassword", name="confirmPassword",
action='/confirm-password', method="POST", ng-cloak)
input(name='_csrf', type='hidden', value=csrfToken)
form-messages(for="confirmPassword")
.form-group
label
| #{translate('password')}
input.form-control(
type='password',
name='password',
required,
placeholder='********',
ng-model="password"
)
span.small.text-primary(
ng-show="confirmPassword.password.$invalid && confirmPassword.password.$dirty"
)
| #{translate("required")}
.actions
button.btn-primary.btn(
style="width: 100%",
type='submit',
ng-disabled="confirmPassword.inflight"
)
span #{translate("confirm")}
.row.row-spaced-small
.col-md-12
p.text-centered
small
| #{translate('confirm_password_footer')}

View file

@ -33,7 +33,7 @@ define [
@rejectChangeIds([ change_id ])
@$scope.$on "change:bulk-accept", (e, change_ids) =>
@acceptChangeIds(change_ids)
@acceptChangeIds(change_ids)
@$scope.$on "change:bulk-reject", (e, change_ids) =>
@rejectChangeIds(change_ids)

View file

@ -555,6 +555,14 @@ describe "AuthenticationController", ->
@AuthenticationController._setRedirectInSession(@req, '/somewhere/specific')
expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific"
describe 'with a png', ->
beforeEach ->
@req = {session: {}}
it 'should not set the redirect', ->
@AuthenticationController._setRedirectInSession(@req, '/something.png')
expect(@req.session.postLoginRedirect).to.equal undefined
describe 'with a js path', ->
beforeEach ->

View file

@ -34,9 +34,8 @@ describe "ClsiCookieManager", ->
ttl:Math.random()
key: "coooookie"
@requires =
"../../infrastructure/RedisWrapper":
client: =>
@redis
"../../infrastructure/RedisWrapper": @RedisWrapper =
client: => @redis
"settings-sharelatex": @settings
"request": @request
@ -61,6 +60,13 @@ describe "ClsiCookieManager", ->
@ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true
done()
it "should _populateServerIdViaRequest if no key is blank", (done)->
@ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1)
@redis.get.callsArgWith(1, null, "")
@ClsiCookieManager._getServerId @project_id, (err, serverId)=>
@ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true
done()
describe "_populateServerIdViaRequest", ->
@ -106,6 +112,24 @@ describe "ClsiCookieManager", ->
@ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=>
@redisMulti.exec.called.should.equal false
done()
it "should also set in the secondary if secondary redis is enabled", (done) ->
@redisSecondaryMulti =
set:sinon.stub()
expire:sinon.stub()
exec:sinon.stub()
@redis_secondary =
multi: => @redisSecondaryMulti
@settings.redis.clsi_cookie_secondary = {}
@RedisWrapper.client = sinon.stub()
@RedisWrapper.client.withArgs("clsi_cookie").returns(@redis)
@RedisWrapper.client.withArgs("clsi_cookie_secondary").returns(@redis_secondary)
@ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires
@ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8")
@ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=>
@redisSecondaryMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true
@redisSecondaryMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsiCookie.ttl).should.equal true
done()
describe "getCookieJar", ->

View file

@ -40,8 +40,8 @@ describe "FileStoreHandler", ->
it "should create read stream", (done)->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "end"
on: (type, cb)->
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@fs.createReadStream.calledWith(@fsPath).should.equal true
@ -51,7 +51,7 @@ describe "FileStoreHandler", ->
@request.returns(@writeStream)
@fs.createReadStream.returns
on: (type, cb)->
if type == "end"
if type == "open"
cb()
pipe:(o)=>
@writeStream.should.equal o
@ -62,7 +62,7 @@ describe "FileStoreHandler", ->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "end"
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@request.args[0][0].method.should.equal "post"
@ -73,7 +73,7 @@ describe "FileStoreHandler", ->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "end"
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@handler._buildUrl.calledWith(@project_id, @file_id).should.equal true
@ -83,7 +83,7 @@ describe "FileStoreHandler", ->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "end"
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err) =>
expect(err).to.not.exist
@ -113,7 +113,7 @@ describe "FileStoreHandler", ->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "end"
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err) =>
expect(err).to.exist

View file

@ -0,0 +1,297 @@
SandboxedModule = require('sandboxed-module')
sinon = require 'sinon'
should = require("chai").should()
expect = require('chai').expect
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
modulePath = '../../../../app/js/Features/SudoMode/SudoModeController'
describe 'SudoModeController', ->
beforeEach ->
@user =
_id: 'abcd'
email: 'user@example.com'
@UserGetter =
getUser: sinon.stub().callsArgWith(2, null, @user)
@SudoModeHandler =
isSudoModeActive: sinon.stub()
activateSudoMode: sinon.stub()
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user._id)
_getRediretFromSession: sinon.stub()
@AuthenticationManager =
authenticate: sinon.stub()
@UserGetter =
getUser: sinon.stub()
@SudoModeController = SandboxedModule.require modulePath, requires:
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'./SudoModeHandler': @SudoModeHandler
'../Authentication/AuthenticationController': @AuthenticationController
'../Authentication/AuthenticationManager': @AuthenticationManager
'../../infrastructure/Mongoose': {mongo: {ObjectId: () -> 'some_object_id'}}
'../User/UserGetter': @UserGetter
describe 'sudoModePrompt', ->
beforeEach ->
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false)
@req = {externalAuthenticationSystemUsed: sinon.stub().returns(false)}
@res = {redirect: sinon.stub(), render: sinon.stub()}
@next = sinon.stub()
it 'should get the logged in user id', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@AuthenticationController.getLoggedInUserId.callCount.should.equal 1
@AuthenticationController.getLoggedInUserId.calledWith(@req).should.equal true
it 'should check if sudo-mode is active', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@SudoModeHandler.isSudoModeActive.callCount.should.equal 1
@SudoModeHandler.isSudoModeActive.calledWith(@user._id).should.equal true
it 'should redirect when sudo-mode is active', ->
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, true)
@SudoModeController.sudoModePrompt(@req, @res, @next)
@res.redirect.callCount.should.equal 1
@res.redirect.calledWith('/project').should.equal true
it 'should render the sudo_mode_prompt page when sudo mode is not active', ->
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false)
@SudoModeController.sudoModePrompt(@req, @res, @next)
@res.render.callCount.should.equal 1
@res.render.calledWith('sudo_mode/sudo_mode_prompt').should.equal true
describe 'when isSudoModeActive produces an error', ->
beforeEach ->
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, new Error('woops'))
@next = sinon.stub()
it 'should call next with an error', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not render page', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@res.render.callCount.should.equal 0
describe 'when external auth system is used', ->
beforeEach ->
@req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
it 'should redirect', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@res.redirect.callCount.should.equal 1
@res.redirect.calledWith('/project').should.equal true
it 'should not check if sudo mode is active', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@SudoModeHandler.isSudoModeActive.callCount.should.equal 0
it 'should not render page', ->
@SudoModeController.sudoModePrompt(@req, @res, @next)
@res.render.callCount.should.equal 0
describe 'submitPassword', ->
beforeEach ->
@AuthenticationController._getRedirectFromSession = sinon.stub().returns '/somewhere'
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
@SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, null)
@password = 'a_terrible_secret'
@req = {body: {password: @password}}
@res = {json: sinon.stub()}
@next = sinon.stub()
describe 'when all goes well', ->
beforeEach ->
it 'should get the logged in user id', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationController.getLoggedInUserId.callCount.should.equal 1
@AuthenticationController.getLoggedInUserId.calledWith(@req).should.equal true
it 'should get redirect from session', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationController._getRedirectFromSession.callCount.should.equal 1
@AuthenticationController._getRedirectFromSession.calledWith(@req).should.equal true
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true
it 'should activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 1
@SudoModeHandler.activateSudoMode.calledWith(@user._id).should.equal true
it 'should send back a json response', ->
@SudoModeController.submitPassword(@req, @res, @next)
@res.json.callCount.should.equal 1
@res.json.calledWith({redir: '/somewhere'}).should.equal true
it 'should not call next', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 0
describe 'when no password is supplied', ->
beforeEach ->
@req.body.password = ''
@next = sinon.stub()
it 'should return next with an error', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 0
it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 0
it 'should not send back a json response', ->
@SudoModeController.submitPassword(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'when getUser produces an error', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
@next = sinon.stub()
it 'should return next with an error', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 0
it 'should not send back a json response', ->
@SudoModeController.submitPassword(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'when getUser does not find a user', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
@next = sinon.stub()
it 'should return next with an error', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 0
it 'should not send back a json response', ->
@SudoModeController.submitPassword(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'when authentication fails', ->
beforeEach ->
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null)
@res.json = sinon.stub()
@req.i18n = {translate: sinon.stub()}
it 'should send back a failure message', ->
@SudoModeController.submitPassword(@req, @res, @next)
@res.json.callCount.should.equal 1
expect(@res.json.lastCall.args[0]).to.have.keys ['message']
expect(@res.json.lastCall.args[0].message).to.have.keys ['text', 'type']
@req.i18n.translate.callCount.should.equal 1
@req.i18n.translate.calledWith('invalid_password')
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true
it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 0
describe 'when authentication produces an error', ->
beforeEach ->
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops'))
@next = sinon.stub()
it 'should return next with an error', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true
it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 0
describe 'when sudo mode activation produces an error', ->
beforeEach ->
@SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, new Error('woops'))
@next = sinon.stub()
it 'should return next with an error', ->
@SudoModeController.submitPassword(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should get the user from storage', ->
@SudoModeController.submitPassword(@req, @res, @next)
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true
it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true
it 'should have tried to activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next)
@SudoModeHandler.activateSudoMode.callCount.should.equal 1
@SudoModeHandler.activateSudoMode.calledWith(@user._id).should.equal true

View file

@ -0,0 +1,188 @@
SandboxedModule = require('sandboxed-module')
assert = require('assert')
require('chai').should()
expect = require('chai').expect
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMode/SudoModeHandler'
describe 'SudoModeHandler', ->
beforeEach ->
@userId = 'some_user_id'
@rclient = {get: sinon.stub(), set: sinon.stub(), del: sinon.stub()}
@RedisWrapper =
client: () => @rclient
@SudoModeHandler = SandboxedModule.require modulePath, requires:
'../../infrastructure/RedisWrapper': @RedisWrapper
'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()}
describe '_buildKey', ->
it 'should build a properly formed key', ->
expect(@SudoModeHandler._buildKey('123')).to.equal 'SudoMode:{123}'
describe 'activateSudoMode', ->
beforeEach ->
@call = (cb) =>
@SudoModeHandler.activateSudoMode @userId, cb
describe 'when all goes well', ->
beforeEach ->
@rclient.set = sinon.stub().callsArgWith(4, null)
it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.equal null
done()
it 'should set a value in redis', (done) ->
@call (err) =>
expect(@rclient.set.callCount).to.equal 1
expect(@rclient.set.calledWith(
'SudoMode:{some_user_id}', '1', 'EX', 60*60
)).to.equal true
done()
describe 'when user id is not supplied', ->
beforeEach ->
@call = (cb) =>
@SudoModeHandler.activateSudoMode null, cb
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
it 'should not set value in redis', (done) ->
@call (err) =>
expect(@rclient.set.callCount).to.equal 0
done()
describe 'when rclient.set produces an error', ->
beforeEach ->
@rclient.set = sinon.stub().callsArgWith(4, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
describe 'clearSudoMode', ->
beforeEach ->
@rclient.del = sinon.stub().callsArgWith(1, null)
@call = (cb) =>
@SudoModeHandler.clearSudoMode @userId, cb
it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.equal null
done()
it 'should delete key from redis', (done) ->
@call (err) =>
expect(@rclient.del.callCount).to.equal 1
expect(@rclient.del.calledWith(
'SudoMode:{some_user_id}'
)).to.equal true
done()
describe 'when rclient.del produces an error', ->
beforeEach ->
@rclient.del = sinon.stub().callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
describe 'when user id is not supplied', ->
beforeEach ->
@call = (cb) =>
@SudoModeHandler.clearSudoMode null, cb
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
it 'should not delete value in redis', (done) ->
@call (err) =>
expect(@rclient.del.callCount).to.equal 0
done()
describe 'isSudoModeActive', ->
beforeEach ->
@call = (cb) =>
@SudoModeHandler.isSudoModeActive @userId, cb
describe 'when sudo-mode is active for that user', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, null, '1')
it 'should not produce an error', (done) ->
@call (err, isActive) =>
expect(err).to.equal null
done()
it 'should get the value from redis', (done) ->
@call (err, isActive) =>
expect(@rclient.get.callCount).to.equal 1
expect(@rclient.get.calledWith('SudoMode:{some_user_id}')).to.equal true
done()
it 'should produce a true result', (done) ->
@call (err, isActive) =>
expect(isActive).to.equal true
done()
describe 'when sudo-mode is not active for that user', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, null, null)
it 'should not produce an error', (done) ->
@call (err, isActive) =>
expect(err).to.equal null
done()
it 'should get the value from redis', (done) ->
@call (err, isActive) =>
expect(@rclient.get.callCount).to.equal 1
expect(@rclient.get.calledWith('SudoMode:{some_user_id}')).to.equal true
done()
it 'should produce a false result', (done) ->
@call (err, isActive) =>
expect(isActive).to.equal false
done()
describe 'when rclient.get produces an error', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, isActive) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
expect(isActive).to.be.oneOf [null, undefined]
done()
describe 'when user id is not supplied', ->
beforeEach ->
@call = (cb) =>
@SudoModeHandler.isSudoModeActive null, cb
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
it 'should not get value in redis', (done) ->
@call (err) =>
expect(@rclient.get.callCount).to.equal 0
done()

View file

@ -0,0 +1,123 @@
SandboxedModule = require('sandboxed-module')
assert = require('assert')
require('chai').should()
expect = require('chai').expect
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMode/SudoModeMiddlewear'
describe 'SudoModeMiddlewear', ->
beforeEach ->
@userId = 'some_user_id'
@SudoModeHandler =
isSudoModeActive: sinon.stub()
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@userId)
_setRedirectInSession: sinon.stub()
@SudoModeMiddlewear = SandboxedModule.require modulePath, requires:
'./SudoModeHandler': @SudoModeHandler
'../Authentication/AuthenticationController': @AuthenticationController
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
describe 'protectPage', ->
beforeEach ->
@externalAuth = false
@call = (cb) =>
@req = {externalAuthenticationSystemUsed: sinon.stub().returns(@externalAuth)}
@res = {redirect: sinon.stub()}
@next = sinon.stub()
@SudoModeMiddlewear.protectPage @req, @res, @next
cb()
describe 'when sudo mode is active', ->
beforeEach ->
@AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId)
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, true)
it 'should get the current user id', (done) ->
@call () =>
@AuthenticationController.getLoggedInUserId.callCount.should.equal 1
done()
it 'should check if sudo-mode is active', (done) ->
@call () =>
@SudoModeHandler.isSudoModeActive.callCount.should.equal 1
@SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true
done()
it 'should call next', (done) ->
@call () =>
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.equal undefined
done()
describe 'when sudo mode is not active', ->
beforeEach ->
@AuthenticationController._setRedirectInSession = sinon.stub()
@AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId)
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false)
it 'should get the current user id', (done) ->
@call () =>
@AuthenticationController.getLoggedInUserId.callCount.should.equal 1
done()
it 'should check if sudo-mode is active', (done) ->
@call () =>
@SudoModeHandler.isSudoModeActive.callCount.should.equal 1
@SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true
done()
it 'should set redirect in session', (done) ->
@call () =>
@AuthenticationController._setRedirectInSession.callCount.should.equal 1
@AuthenticationController._setRedirectInSession.calledWith(@req).should.equal true
done()
it 'should redirect to the password-prompt page', (done) ->
@call () =>
@res.redirect.callCount.should.equal 1
@res.redirect.calledWith('/confirm-password').should.equal true
done()
describe 'when isSudoModeActive produces an error', ->
beforeEach ->
@AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId)
@SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, new Error('woops'))
it 'should get the current user id', (done) ->
@call () =>
@AuthenticationController.getLoggedInUserId.callCount.should.equal 1
done()
it 'should check if sudo-mode is active', (done) ->
@call () =>
@SudoModeHandler.isSudoModeActive.callCount.should.equal 1
@SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true
done()
it 'should call next with an error', (done) ->
@call () =>
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
done()
describe 'when external auth is being used', ->
beforeEach ->
@externalAuth = true
it 'should immediately return next with no args', (done) ->
@call () =>
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.not.exist
done()
it 'should not get the current user id', (done) ->
@call () =>
@AuthenticationController.getLoggedInUserId.callCount.should.equal 0
done()
it 'should not check if sudo-mode is active', (done) ->
@call () =>
@SudoModeHandler.isSudoModeActive.callCount.should.equal 0
done()

View file

@ -60,6 +60,8 @@ describe "UserController", ->
trackSession: sinon.stub()
untrackSession: sinon.stub()
revokeAllUserSessions: sinon.stub().callsArgWith(2, null)
@SudoModeHandler =
clearSudoMode: sinon.stub()
@UserController = SandboxedModule.require modulePath, requires:
"./UserLocator": @UserLocator
"./UserDeleter": @UserDeleter
@ -73,6 +75,7 @@ describe "UserController", ->
"../Subscription/SubscriptionDomainHandler":@SubscriptionDomainHandler
"./UserHandler":@UserHandler
"./UserSessionsManager": @UserSessionsManager
"../SudoMode/SudoModeHandler": @SudoModeHandler
"settings-sharelatex": @settings
"logger-sharelatex":
log:->
@ -302,6 +305,17 @@ describe "UserController", ->
@UserController.logout @req, @res
it 'should clear sudo-mode', (done) ->
@req.session.destroy = sinon.stub().callsArgWith(0)
@SudoModeHandler.clearSudoMode = sinon.stub()
@res.redirect = (url)=>
url.should.equal "/login"
@SudoModeHandler.clearSudoMode.callCount.should.equal 1
@SudoModeHandler.clearSudoMode.calledWith(@user._id).should.equal true
done()
@UserController.logout @req, @res
describe "register", ->
beforeEach ->

View file

@ -34,9 +34,9 @@ describe "Sessions", ->
expect(sessions[0].slice(0, 5)).to.equal 'sess:'
next()
# should be able to access settings page
# should be able to access project list page
, (next) =>
@user1.getUserSettingsPage (err, statusCode) =>
@user1.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()
@ -94,15 +94,15 @@ describe "Sessions", ->
expect(sessions[1].slice(0, 5)).to.equal 'sess:'
next()
# both should be able to access settings page
# both should be able to access project list page
, (next) =>
@user1.getUserSettingsPage (err, statusCode) =>
@user1.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()
, (next) =>
@user2.getUserSettingsPage (err, statusCode) =>
@user2.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()
@ -117,16 +117,16 @@ describe "Sessions", ->
expect(sessions.length).to.equal 1
next()
# first session should not have access to settings page
# first session should not have access to project list page
, (next) =>
@user1.getUserSettingsPage (err, statusCode) =>
@user1.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
# second session should still have access to settings
, (next) =>
@user2.getUserSettingsPage (err, statusCode) =>
@user2.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()
@ -141,9 +141,9 @@ describe "Sessions", ->
expect(sessions.length).to.equal 0
next()
# second session should not have access to settings page
# second session should not have access to project list page
, (next) =>
@user2.getUserSettingsPage (err, statusCode) =>
@user2.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
@ -216,22 +216,22 @@ describe "Sessions", ->
expect(sessions.length).to.equal 1
next()
# users one and three should not be able to access settings page
# users one and three should not be able to access project list page
, (next) =>
@user1.getUserSettingsPage (err, statusCode) =>
@user1.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
, (next) =>
@user3.getUserSettingsPage (err, statusCode) =>
@user3.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
# user two should still be logged in, and able to access settings page
# user two should still be logged in, and able to access project list page
, (next) =>
@user2.getUserSettingsPage (err, statusCode) =>
@user2.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()
@ -305,6 +305,19 @@ describe "Sessions", ->
expect(sessions[1].slice(0, 5)).to.equal 'sess:'
next()
# enter sudo-mode
, (next) =>
@user2.getCsrfToken (err) =>
expect(err).to.be.oneOf [null, undefined]
@user2.request.post {
uri: '/confirm-password',
json:
password: @user2.password
}, (err, response, body) =>
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
next()
# check the sessions page
, (next) =>
@user2.request.get {
@ -328,22 +341,22 @@ describe "Sessions", ->
expect(sessions.length).to.equal 1
next()
# users one and three should not be able to access settings page
# users one and three should not be able to access project list page
, (next) =>
@user1.getUserSettingsPage (err, statusCode) =>
@user1.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
, (next) =>
@user3.getUserSettingsPage (err, statusCode) =>
@user3.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 302
next()
# user two should still be logged in, and able to access settings page
# user two should still be logged in, and able to access project list page
, (next) =>
@user2.getUserSettingsPage (err, statusCode) =>
@user2.getProjectListPage (err, statusCode) =>
expect(err).to.equal null
expect(statusCode).to.equal 200
next()

View file

@ -134,6 +134,15 @@ class User
return callback(error) if error?
callback(null, response.statusCode)
getProjectListPage: (callback=(error, statusCode)->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@request.get {
url: "/project"
}, (error, response, body) =>
return callback(error) if error?
callback(null, response.statusCode)
module.exports = User