Jelajahi Sumber

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

Enable sudo-mode for v2
Shane Kilkelly 8 tahun lalu
induk
melakukan
4c2a90966a

+ 9 - 5
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()

+ 3 - 3
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)

+ 13 - 0
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'))

+ 2 - 1
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)

+ 27 - 0
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)

+ 3 - 0
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]

+ 6 - 2
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(

+ 14 - 1
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()

+ 13 - 0
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) ->

+ 1 - 0
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"

+ 16 - 17
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)

+ 20 - 0
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) =>

+ 1 - 0
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 ->