Просмотр исходного кода

Merge branch 'master' into pr-bulk-actions

Paulo Reis 9 лет назад
Родитель
Сommit
e532f543a3
20 измененных файлов с 931 добавлено и 63 удалено
  1. 5 1
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  2. 12 5
      services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee
  3. 22 19
      services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee
  4. 63 0
      services/web/app/coffee/Features/SudoMode/SudoModeController.coffee
  5. 33 0
      services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee
  6. 24 0
      services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee
  7. 2 0
      services/web/app/coffee/Features/User/UserController.coffee
  8. 5 5
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  9. 13 2
      services/web/app/coffee/router.coffee
  10. 45 0
      services/web/app/views/sudo_mode/sudo_mode_prompt.pug
  11. 1 1
      services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
  12. 8 0
      services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
  13. 27 3
      services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee
  14. 7 7
      services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee
  15. 297 0
      services/web/test/UnitTests/coffee/SudoMode/SudoModeControllerTests.coffee
  16. 188 0
      services/web/test/UnitTests/coffee/SudoMode/SudoModeHandlerTests.coffee
  17. 123 0
      services/web/test/UnitTests/coffee/SudoMode/SudoModeMiddlewearTests.coffee
  18. 14 0
      services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
  19. 33 20
      services/web/test/acceptance/coffee/SessionTests.coffee
  20. 9 0
      services/web/test/acceptance/coffee/helpers/User.coffee

+ 5 - 1
services/web/app/coffee/Features/Authentication/AuthenticationController.coffee

@@ -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) ->

+ 12 - 5
services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee

@@ -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

+ 22 - 19
services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee

@@ -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"

+ 63 - 0
services/web/app/coffee/Features/SudoMode/SudoModeController.coffee

@@ -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'
+						}
+					}
+

+ 33 - 0
services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee

@@ -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')

+ 24 - 0
services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee

@@ -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')

+ 2 - 0
services/web/app/coffee/Features/User/UserController.coffee

@@ -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) ->)->

+ 5 - 5
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -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?

+ 13 - 2
services/web/app/coffee/router.coffee

@@ -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

+ 45 - 0
services/web/app/views/sudo_mode/sudo_mode_prompt.pug

@@ -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')}

+ 1 - 1
services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee

@@ -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)

+ 8 - 0
services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee

@@ -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 ->

+ 27 - 3
services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee

@@ -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", ->
 

+ 7 - 7
services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee

@@ -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

+ 297 - 0
services/web/test/UnitTests/coffee/SudoMode/SudoModeControllerTests.coffee

@@ -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

+ 188 - 0
services/web/test/UnitTests/coffee/SudoMode/SudoModeHandlerTests.coffee

@@ -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()

+ 123 - 0
services/web/test/UnitTests/coffee/SudoMode/SudoModeMiddlewearTests.coffee

@@ -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()

+ 14 - 0
services/web/test/UnitTests/coffee/User/UserControllerTests.coffee

@@ -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 ->

+ 33 - 20
services/web/test/acceptance/coffee/SessionTests.coffee

@@ -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()

+ 9 - 0
services/web/test/acceptance/coffee/helpers/User.coffee

@@ -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