diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index d301ddabbc..bc44e09498 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -12,6 +12,7 @@ UserSessionsManager = require("../User/UserSessionsManager") Analytics = require "../Analytics/AnalyticsManager" passport = require 'passport' NotificationsBuilder = require("../Notifications/NotificationsBuilder") +SudoModeHandler = require '../SudoMode/SudoModeHandler' module.exports = AuthenticationController = @@ -76,11 +77,14 @@ module.exports = AuthenticationController = AuthenticationController.afterLoginSessionSetup req, user, (err) -> if err? return next(err) - AuthenticationController._clearRedirectFromSession(req) - if req.headers?['accept']?.match(/^application\/json.*$/) - res.json {redir: redir} - else - res.redirect(redir) + SudoModeHandler.activateSudoMode user._id, (err) -> + if err? + logger.err {err, user_id: user._id}, "Error activating Sudo Mode on login, continuing" + AuthenticationController._clearRedirectFromSession(req) + if req.headers?['accept']?.match(/^application\/json.*$/) + res.json {redir: redir} + else + res.redirect(redir) doPassportLogin: (req, username, password, done) -> email = username.toLowerCase() diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee index da31522a6c..b26b88e4f2 100644 --- a/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee +++ b/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee @@ -1,15 +1,15 @@ 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' +Settings = require 'settings-sharelatex' module.exports = SudoModeController = sudoModePrompt: (req, res, next) -> - if req.externalAuthenticationSystemUsed() + if req.externalAuthenticationSystemUsed() and !Settings.overleaf? logger.log {userId}, "[SudoMode] using external auth, redirecting" return res.redirect('/project') userId = AuthenticationController.getLoggedInUserId(req) @@ -39,7 +39,7 @@ module.exports = SudoModeController = 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) -> + SudoModeHandler.authenticate userRecord.email, password, (err, user) -> if err? logger.err {err, userId}, "[SudoMode] error authenticating user" return next(err) diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee index 004b63ccc2..18e7bfd74f 100644 --- a/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee +++ b/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee @@ -1,6 +1,10 @@ RedisWrapper = require('../../infrastructure/RedisWrapper') rclient = RedisWrapper.client('sudomode') 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 @@ -11,6 +15,15 @@ module.exports = SudoModeHandler = _buildKey: (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)->) -> if !userId? return callback(new Error('[SudoMode] user must be supplied')) diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee index 479d67eee0..64d238b543 100644 --- a/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee +++ b/services/web/app/coffee/Features/SudoMode/SudoModeMiddlewear.coffee @@ -1,12 +1,13 @@ logger = require 'logger-sharelatex' SudoModeHandler = require './SudoModeHandler' AuthenticationController = require '../Authentication/AuthenticationController' +Settings = require 'settings-sharelatex' module.exports = SudoModeMiddlewear = protectPage: (req, res, next) -> - if req.externalAuthenticationSystemUsed() + if req.externalAuthenticationSystemUsed() and !Settings.overleaf? logger.log {userId}, "[SudoMode] using external auth, skipping sudo-mode check" return next() userId = AuthenticationController.getLoggedInUserId(req) diff --git a/services/web/app/coffee/Features/V1/V1Handler.coffee b/services/web/app/coffee/Features/V1/V1Handler.coffee new file mode 100644 index 0000000000..d4f4286f32 --- /dev/null +++ b/services/web/app/coffee/Features/V1/V1Handler.coffee @@ -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) diff --git a/services/web/app/views/sudo_mode/sudo_mode_prompt.pug b/services/web/app/views/sudo_mode/sudo_mode_prompt.pug index 27ccc41eb6..63c20fc33a 100644 --- a/services/web/app/views/sudo_mode/sudo_mode_prompt.pug +++ b/services/web/app/views/sudo_mode/sudo_mode_prompt.pug @@ -43,3 +43,6 @@ block content p.text-centered small | #{translate('confirm_password_footer')} + + p.text-centered + small #[a(href='/user/password/reset' target="_blank") Set or reset password] diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee index 07a431a229..fb628c0c0c 100644 --- a/services/web/test/acceptance/coffee/SessionTests.coffee +++ b/services/web/test/acceptance/coffee/SessionTests.coffee @@ -4,6 +4,7 @@ User = require "./helpers/User" request = require "./helpers/request" settings = require "settings-sharelatex" redis = require "./helpers/redis" +MockV1Api = require './helpers/MockV1Api' describe "Sessions", -> before (done) -> @@ -254,7 +255,7 @@ describe "Sessions", -> describe 'three sessions, sessions page', -> - before -> + before (done) -> # set up second session for this user @user2 = new User() @user2.email = @user1.email @@ -262,7 +263,10 @@ describe "Sessions", -> @user3 = new User() @user3.email = @user1.email @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) -> async.series( diff --git a/services/web/test/acceptance/coffee/SettingsTests.coffee b/services/web/test/acceptance/coffee/SettingsTests.coffee index bd2942e072..78349b0952 100644 --- a/services/web/test/acceptance/coffee/SettingsTests.coffee +++ b/services/web/test/acceptance/coffee/SettingsTests.coffee @@ -1,18 +1,31 @@ should = require('chai').should() async = require("async") User = require "./helpers/User" +MockV1Api = require './helpers/MockV1Api' describe 'SettingsPage', -> before (done) -> @user = new User() + @v1Id = 1234 + @v1User = + id: @v1Id + email: @user.email + password: @user.password + profile: + id: @v1Id + email: @user.email async.series [ @user.ensureUserExists.bind(@user) @user.login.bind(@user) + (cb) => @user.mongoUpdate {$set: {'overleaf.id': @v1Id}}, cb + (cb) => + MockV1Api.setUser @v1Id, @v1User + cb() @user.activateSudoMode.bind(@user) ], done - it 'load settigns page', (done) -> + it 'load settings page', (done) -> @user.getUserSettingsPage (err, statusCode) -> statusCode.should.equal 200 done() diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index 14177dfbb4..82e465193f 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -76,6 +76,19 @@ module.exports = MockV1Api = @updateEmail parseInt(req.params.id), email 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) -> throw error if error? .on "error", (error) -> diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 300a4663e7..78f7b288f6 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -29,6 +29,7 @@ describe "AuthenticationController", -> untrackSession: sinon.stub() revokeAllUserSessions: sinon.stub().callsArgWith(1, null) "../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} + "../SudoMode/SudoModeHandler": @SudoModeHandler = {activateSudoMode: sinon.stub().callsArgWith(1, null)} @user = _id: ObjectId() email: @email = "USER@example.com" diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee index c1de2e278c..2532a40898 100644 --- a/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee +++ b/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee @@ -14,22 +14,21 @@ describe 'SudoModeController', -> @UserGetter = getUser: sinon.stub().callsArgWith(2, null, @user) @SudoModeHandler = + authenticate: sinon.stub() 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 + 'settings-sharelatex': @Settings = {} describe 'sudoModePrompt', -> beforeEach -> @@ -95,7 +94,7 @@ describe 'SudoModeController', -> beforeEach -> @AuthenticationController._getRedirectFromSession = sinon.stub().returns '/somewhere' @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) @password = 'a_terrible_secret' @req = {body: {password: @password}} @@ -122,8 +121,8 @@ describe 'SudoModeController', -> 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 + @SudoModeHandler.authenticate.callCount.should.equal 1 + @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true it 'should activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -155,7 +154,7 @@ describe 'SudoModeController', -> it 'should not try to authenticate the user with the password', -> @SudoModeController.submitPassword(@req, @res, @next) - @AuthenticationManager.authenticate.callCount.should.equal 0 + @SudoModeHandler.authenticate.callCount.should.equal 0 it 'should not activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -182,7 +181,7 @@ describe 'SudoModeController', -> it 'should not try to authenticate the user with the password', -> @SudoModeController.submitPassword(@req, @res, @next) - @AuthenticationManager.authenticate.callCount.should.equal 0 + @SudoModeHandler.authenticate.callCount.should.equal 0 it 'should not activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -209,7 +208,7 @@ describe 'SudoModeController', -> it 'should not try to authenticate the user with the password', -> @SudoModeController.submitPassword(@req, @res, @next) - @AuthenticationManager.authenticate.callCount.should.equal 0 + @SudoModeHandler.authenticate.callCount.should.equal 0 it 'should not activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -221,7 +220,7 @@ describe 'SudoModeController', -> describe 'when authentication fails', -> beforeEach -> - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null) + @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, null, null) @res.json = sinon.stub() @req.i18n = {translate: sinon.stub()} @@ -240,8 +239,8 @@ describe 'SudoModeController', -> 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 + @SudoModeHandler.authenticate.callCount.should.equal 1 + @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true it 'should not activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -249,7 +248,7 @@ describe 'SudoModeController', -> describe 'when authentication produces an error', -> beforeEach -> - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) + @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) @next = sinon.stub() it 'should return next with an error', -> @@ -264,8 +263,8 @@ describe 'SudoModeController', -> 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 + @SudoModeHandler.authenticate.callCount.should.equal 1 + @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true it 'should not activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) @@ -288,8 +287,8 @@ describe 'SudoModeController', -> 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 + @SudoModeHandler.authenticate.callCount.should.equal 1 + @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true it 'should have tried to activate sudo mode', -> @SudoModeController.submitPassword(@req, @res, @next) diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee index e7f3ccd150..e23797087f 100644 --- a/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee +++ b/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee @@ -9,12 +9,20 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMo describe 'SudoModeHandler', -> beforeEach -> @userId = 'some_user_id' + @email = 'someuser@example.com' + @user = + _id: @userId + email: @email @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()} + '../Authentication/AuthenticationManager': @AuthenticationManager = {} + 'settings-sharelatex': @Settings = {} + '../V1/V1Handler': @V1Handler = {authWithV1: sinon.stub()} + '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} describe '_buildKey', -> @@ -115,6 +123,18 @@ describe 'SudoModeHandler', -> expect(@rclient.del.callCount).to.equal 0 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', -> beforeEach -> @call = (cb) => diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee index d17c397d3c..cc4f859ab2 100644 --- a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee +++ b/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewearTests.coffee @@ -18,6 +18,7 @@ describe 'SudoModeMiddlewear', -> './SudoModeHandler': @SudoModeHandler '../Authentication/AuthenticationController': @AuthenticationController 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} + 'settings-sharelatex': @Settings = {} describe 'protectPage', -> beforeEach ->