Merge pull request #918 from sharelatex/sk-enable-sudo-mode-in-v2

Enable sudo-mode for v2
This commit is contained in:
Shane Kilkelly 2018-10-05 10:05:24 +01:00 committed by GitHub
commit 4c2a90966a
13 changed files with 128 additions and 29 deletions

View file

@ -12,6 +12,7 @@ UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager" Analytics = require "../Analytics/AnalyticsManager"
passport = require 'passport' passport = require 'passport'
NotificationsBuilder = require("../Notifications/NotificationsBuilder") NotificationsBuilder = require("../Notifications/NotificationsBuilder")
SudoModeHandler = require '../SudoMode/SudoModeHandler'
module.exports = AuthenticationController = module.exports = AuthenticationController =
@ -76,11 +77,14 @@ module.exports = AuthenticationController =
AuthenticationController.afterLoginSessionSetup req, user, (err) -> AuthenticationController.afterLoginSessionSetup req, user, (err) ->
if err? if err?
return next(err) return next(err)
AuthenticationController._clearRedirectFromSession(req) SudoModeHandler.activateSudoMode user._id, (err) ->
if req.headers?['accept']?.match(/^application\/json.*$/) if err?
res.json {redir: redir} logger.err {err, user_id: user._id}, "Error activating Sudo Mode on login, continuing"
else AuthenticationController._clearRedirectFromSession(req)
res.redirect(redir) if req.headers?['accept']?.match(/^application\/json.*$/)
res.json {redir: redir}
else
res.redirect(redir)
doPassportLogin: (req, username, password, done) -> doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase() email = username.toLowerCase()

View file

@ -1,15 +1,15 @@
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
SudoModeHandler = require './SudoModeHandler' SudoModeHandler = require './SudoModeHandler'
AuthenticationController = require '../Authentication/AuthenticationController' AuthenticationController = require '../Authentication/AuthenticationController'
AuthenticationManager = require '../Authentication/AuthenticationManager'
ObjectId = require('../../infrastructure/Mongoose').mongo.ObjectId ObjectId = require('../../infrastructure/Mongoose').mongo.ObjectId
UserGetter = require '../User/UserGetter' UserGetter = require '../User/UserGetter'
Settings = require 'settings-sharelatex'
module.exports = SudoModeController = module.exports = SudoModeController =
sudoModePrompt: (req, res, next) -> sudoModePrompt: (req, res, next) ->
if req.externalAuthenticationSystemUsed() if req.externalAuthenticationSystemUsed() and !Settings.overleaf?
logger.log {userId}, "[SudoMode] using external auth, redirecting" logger.log {userId}, "[SudoMode] using external auth, redirecting"
return res.redirect('/project') return res.redirect('/project')
userId = AuthenticationController.getLoggedInUserId(req) userId = AuthenticationController.getLoggedInUserId(req)
@ -39,7 +39,7 @@ module.exports = SudoModeController =
err = new Error('user not found') err = new Error('user not found')
logger.err {err, userId}, "[SudoMode] user not found" logger.err {err, userId}, "[SudoMode] user not found"
return next(err) return next(err)
AuthenticationManager.authenticate email: userRecord.email, password, (err, user) -> SudoModeHandler.authenticate userRecord.email, password, (err, user) ->
if err? if err?
logger.err {err, userId}, "[SudoMode] error authenticating user" logger.err {err, userId}, "[SudoMode] error authenticating user"
return next(err) return next(err)

View file

@ -1,6 +1,10 @@
RedisWrapper = require('../../infrastructure/RedisWrapper') RedisWrapper = require('../../infrastructure/RedisWrapper')
rclient = RedisWrapper.client('sudomode') rclient = RedisWrapper.client('sudomode')
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
AuthenticationManager = require '../Authentication/AuthenticationManager'
Settings = require 'settings-sharelatex'
V1Handler = require '../V1/V1Handler'
UserGetter = require '../User/UserGetter'
TIMEOUT_IN_SECONDS = 60 * 60 TIMEOUT_IN_SECONDS = 60 * 60
@ -11,6 +15,15 @@ module.exports = SudoModeHandler =
_buildKey: (userId) -> _buildKey: (userId) ->
"SudoMode:{#{userId}}" "SudoMode:{#{userId}}"
authenticate: (email, password, callback=(err, user)->) ->
if Settings.overleaf?
V1Handler.authWithV1 email, password, (err, isValid, v1Profile) ->
if !isValid
return callback(null, null)
UserGetter.getUser {'overleaf.id': v1Profile.id}, callback
else
AuthenticationManager.authenticate {email}, password, callback
activateSudoMode: (userId, callback=(err)->) -> activateSudoMode: (userId, callback=(err)->) ->
if !userId? if !userId?
return callback(new Error('[SudoMode] user must be supplied')) return callback(new Error('[SudoMode] user must be supplied'))

View file

@ -1,12 +1,13 @@
logger = require 'logger-sharelatex' logger = require 'logger-sharelatex'
SudoModeHandler = require './SudoModeHandler' SudoModeHandler = require './SudoModeHandler'
AuthenticationController = require '../Authentication/AuthenticationController' AuthenticationController = require '../Authentication/AuthenticationController'
Settings = require 'settings-sharelatex'
module.exports = SudoModeMiddlewear = module.exports = SudoModeMiddlewear =
protectPage: (req, res, next) -> protectPage: (req, res, next) ->
if req.externalAuthenticationSystemUsed() if req.externalAuthenticationSystemUsed() and !Settings.overleaf?
logger.log {userId}, "[SudoMode] using external auth, skipping sudo-mode check" logger.log {userId}, "[SudoMode] using external auth, skipping sudo-mode check"
return next() return next()
userId = AuthenticationController.getLoggedInUserId(req) userId = AuthenticationController.getLoggedInUserId(req)

View file

@ -0,0 +1,27 @@
V1Api = require './V1Api'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
module.exports = V1Handler =
authWithV1: (email, password, callback=(err, isValid, v1Profile)->) ->
V1Api.request {
method: 'POST',
url: '/api/v1/sharelatex/login',
json: {email, password},
expectedStatusCodes: [403]
}, (err, response, body) ->
if err?
logger.err {email, err},
"[V1Handler] error while talking to v1 login api"
return callback(err)
if response.statusCode in [200, 403]
isValid = body.valid
userProfile = body.user_profile
logger.log {email, isValid, v1UserId: body?.user_profile?.id},
"[V1Handler] got response from v1 login api"
callback(null, isValid, userProfile)
else
err = new Error("Unexpected status from v1 login api: #{response.statusCode}")
callback(err)

View file

@ -43,3 +43,6 @@ block content
p.text-centered p.text-centered
small small
| #{translate('confirm_password_footer')} | #{translate('confirm_password_footer')}
p.text-centered
small #[a(href='/user/password/reset' target="_blank") Set or reset password]

View file

@ -4,6 +4,7 @@ User = require "./helpers/User"
request = require "./helpers/request" request = require "./helpers/request"
settings = require "settings-sharelatex" settings = require "settings-sharelatex"
redis = require "./helpers/redis" redis = require "./helpers/redis"
MockV1Api = require './helpers/MockV1Api'
describe "Sessions", -> describe "Sessions", ->
before (done) -> before (done) ->
@ -254,7 +255,7 @@ describe "Sessions", ->
describe 'three sessions, sessions page', -> describe 'three sessions, sessions page', ->
before -> before (done) ->
# set up second session for this user # set up second session for this user
@user2 = new User() @user2 = new User()
@user2.email = @user1.email @user2.email = @user1.email
@ -262,7 +263,10 @@ describe "Sessions", ->
@user3 = new User() @user3 = new User()
@user3.email = @user1.email @user3.email = @user1.email
@user3.password = @user1.password @user3.password = @user1.password
async.series [
@user2.login.bind(@user2)
@user2.activateSudoMode.bind(@user2)
], done
it "should allow the user to erase the other two sessions", (done) -> it "should allow the user to erase the other two sessions", (done) ->
async.series( async.series(

View file

@ -1,18 +1,31 @@
should = require('chai').should() should = require('chai').should()
async = require("async") async = require("async")
User = require "./helpers/User" User = require "./helpers/User"
MockV1Api = require './helpers/MockV1Api'
describe 'SettingsPage', -> describe 'SettingsPage', ->
before (done) -> before (done) ->
@user = new User() @user = new User()
@v1Id = 1234
@v1User =
id: @v1Id
email: @user.email
password: @user.password
profile:
id: @v1Id
email: @user.email
async.series [ async.series [
@user.ensureUserExists.bind(@user) @user.ensureUserExists.bind(@user)
@user.login.bind(@user) @user.login.bind(@user)
(cb) => @user.mongoUpdate {$set: {'overleaf.id': @v1Id}}, cb
(cb) =>
MockV1Api.setUser @v1Id, @v1User
cb()
@user.activateSudoMode.bind(@user) @user.activateSudoMode.bind(@user)
], done ], done
it 'load settigns page', (done) -> it 'load settings page', (done) ->
@user.getUserSettingsPage (err, statusCode) -> @user.getUserSettingsPage (err, statusCode) ->
statusCode.should.equal 200 statusCode.should.equal 200
done() done()

View file

@ -76,6 +76,19 @@ module.exports = MockV1Api =
@updateEmail parseInt(req.params.id), email @updateEmail parseInt(req.params.id), email
return res.sendStatus 200 return res.sendStatus 200
app.post "/api/v1/sharelatex/login", (req, res, next) =>
for id, user of @users
if user? && user.email == req.body.email && user.password == req.body.password
return res.json {
email: user.email,
valid: true,
user_profile: user.profile
}
return res.status(403).json {
email: req.body.email,
valid: false
}
app.listen 5000, (error) -> app.listen 5000, (error) ->
throw error if error? throw error if error?
.on "error", (error) -> .on "error", (error) ->

View file

@ -29,6 +29,7 @@ describe "AuthenticationController", ->
untrackSession: sinon.stub() untrackSession: sinon.stub()
revokeAllUserSessions: sinon.stub().callsArgWith(1, null) revokeAllUserSessions: sinon.stub().callsArgWith(1, null)
"../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} "../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}}
"../SudoMode/SudoModeHandler": @SudoModeHandler = {activateSudoMode: sinon.stub().callsArgWith(1, null)}
@user = @user =
_id: ObjectId() _id: ObjectId()
email: @email = "USER@example.com" email: @email = "USER@example.com"

View file

@ -14,22 +14,21 @@ describe 'SudoModeController', ->
@UserGetter = @UserGetter =
getUser: sinon.stub().callsArgWith(2, null, @user) getUser: sinon.stub().callsArgWith(2, null, @user)
@SudoModeHandler = @SudoModeHandler =
authenticate: sinon.stub()
isSudoModeActive: sinon.stub() isSudoModeActive: sinon.stub()
activateSudoMode: sinon.stub() activateSudoMode: sinon.stub()
@AuthenticationController = @AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user._id) getLoggedInUserId: sinon.stub().returns(@user._id)
_getRediretFromSession: sinon.stub() _getRediretFromSession: sinon.stub()
@AuthenticationManager =
authenticate: sinon.stub()
@UserGetter = @UserGetter =
getUser: sinon.stub() getUser: sinon.stub()
@SudoModeController = SandboxedModule.require modulePath, requires: @SudoModeController = SandboxedModule.require modulePath, requires:
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'./SudoModeHandler': @SudoModeHandler './SudoModeHandler': @SudoModeHandler
'../Authentication/AuthenticationController': @AuthenticationController '../Authentication/AuthenticationController': @AuthenticationController
'../Authentication/AuthenticationManager': @AuthenticationManager
'../../infrastructure/Mongoose': {mongo: {ObjectId: () -> 'some_object_id'}} '../../infrastructure/Mongoose': {mongo: {ObjectId: () -> 'some_object_id'}}
'../User/UserGetter': @UserGetter '../User/UserGetter': @UserGetter
'settings-sharelatex': @Settings = {}
describe 'sudoModePrompt', -> describe 'sudoModePrompt', ->
beforeEach -> beforeEach ->
@ -95,7 +94,7 @@ describe 'SudoModeController', ->
beforeEach -> beforeEach ->
@AuthenticationController._getRedirectFromSession = sinon.stub().returns '/somewhere' @AuthenticationController._getRedirectFromSession = sinon.stub().returns '/somewhere'
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, null, @user)
@SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, null) @SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, null)
@password = 'a_terrible_secret' @password = 'a_terrible_secret'
@req = {body: {password: @password}} @req = {body: {password: @password}}
@ -122,8 +121,8 @@ describe 'SudoModeController', ->
it 'should try to authenticate the user with the password', -> it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1 @SudoModeHandler.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true
it 'should activate sudo mode', -> it 'should activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -155,7 +154,7 @@ describe 'SudoModeController', ->
it 'should not try to authenticate the user with the password', -> it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0 @SudoModeHandler.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', -> it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -182,7 +181,7 @@ describe 'SudoModeController', ->
it 'should not try to authenticate the user with the password', -> it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0 @SudoModeHandler.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', -> it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -209,7 +208,7 @@ describe 'SudoModeController', ->
it 'should not try to authenticate the user with the password', -> it 'should not try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 0 @SudoModeHandler.authenticate.callCount.should.equal 0
it 'should not activate sudo mode', -> it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -221,7 +220,7 @@ describe 'SudoModeController', ->
describe 'when authentication fails', -> describe 'when authentication fails', ->
beforeEach -> beforeEach ->
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null) @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, null, null)
@res.json = sinon.stub() @res.json = sinon.stub()
@req.i18n = {translate: sinon.stub()} @req.i18n = {translate: sinon.stub()}
@ -240,8 +239,8 @@ describe 'SudoModeController', ->
it 'should try to authenticate the user with the password', -> it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1 @SudoModeHandler.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true
it 'should not activate sudo mode', -> it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -249,7 +248,7 @@ describe 'SudoModeController', ->
describe 'when authentication produces an error', -> describe 'when authentication produces an error', ->
beforeEach -> beforeEach ->
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, new Error('woops'))
@next = sinon.stub() @next = sinon.stub()
it 'should return next with an error', -> it 'should return next with an error', ->
@ -264,8 +263,8 @@ describe 'SudoModeController', ->
it 'should try to authenticate the user with the password', -> it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1 @SudoModeHandler.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true
it 'should not activate sudo mode', -> it 'should not activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@ -288,8 +287,8 @@ describe 'SudoModeController', ->
it 'should try to authenticate the user with the password', -> it 'should try to authenticate the user with the password', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)
@AuthenticationManager.authenticate.callCount.should.equal 1 @SudoModeHandler.authenticate.callCount.should.equal 1
@AuthenticationManager.authenticate.calledWith({email: @user.email}, @password).should.equal true @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true
it 'should have tried to activate sudo mode', -> it 'should have tried to activate sudo mode', ->
@SudoModeController.submitPassword(@req, @res, @next) @SudoModeController.submitPassword(@req, @res, @next)

View file

@ -9,12 +9,20 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMo
describe 'SudoModeHandler', -> describe 'SudoModeHandler', ->
beforeEach -> beforeEach ->
@userId = 'some_user_id' @userId = 'some_user_id'
@email = 'someuser@example.com'
@user =
_id: @userId
email: @email
@rclient = {get: sinon.stub(), set: sinon.stub(), del: sinon.stub()} @rclient = {get: sinon.stub(), set: sinon.stub(), del: sinon.stub()}
@RedisWrapper = @RedisWrapper =
client: () => @rclient client: () => @rclient
@SudoModeHandler = SandboxedModule.require modulePath, requires: @SudoModeHandler = SandboxedModule.require modulePath, requires:
'../../infrastructure/RedisWrapper': @RedisWrapper '../../infrastructure/RedisWrapper': @RedisWrapper
'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()} 'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()}
'../Authentication/AuthenticationManager': @AuthenticationManager = {}
'settings-sharelatex': @Settings = {}
'../V1/V1Handler': @V1Handler = {authWithV1: sinon.stub()}
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
describe '_buildKey', -> describe '_buildKey', ->
@ -115,6 +123,18 @@ describe 'SudoModeHandler', ->
expect(@rclient.del.callCount).to.equal 0 expect(@rclient.del.callCount).to.equal 0
done() done()
describe 'authenticate', ->
beforeEach ->
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
it 'should call AuthenticationManager.authenticate', (done) ->
@SudoModeHandler.authenticate @email, 'password', (err, user) =>
expect(err).to.not.exist
expect(user).to.exist
expect(user).to.deep.equal @user
expect(@AuthenticationManager.authenticate.callCount).to.equal 1
done()
describe 'isSudoModeActive', -> describe 'isSudoModeActive', ->
beforeEach -> beforeEach ->
@call = (cb) => @call = (cb) =>

View file

@ -18,6 +18,7 @@ describe 'SudoModeMiddlewear', ->
'./SudoModeHandler': @SudoModeHandler './SudoModeHandler': @SudoModeHandler
'../Authentication/AuthenticationController': @AuthenticationController '../Authentication/AuthenticationController': @AuthenticationController
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'settings-sharelatex': @Settings = {}
describe 'protectPage', -> describe 'protectPage', ->
beforeEach -> beforeEach ->