Merge branch 'sk-sudo-mode'

This commit is contained in:
Shane Kilkelly
2017-05-17 09:48:22 +01:00
13 changed files with 849 additions and 27 deletions
@@ -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'
}
}
@@ -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')
@@ -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')
@@ -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) ->)->
@@ -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
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
@@ -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')}
@@ -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
@@ -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()
@@ -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()
@@ -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 ->
@@ -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()
@@ -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