diff --git a/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee b/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee new file mode 100644 index 0000000000..4a3ce18b5e --- /dev/null +++ b/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee @@ -0,0 +1,21 @@ +request = require 'request' +logger = require 'logger-sharelatex' +Settings = require 'settings-sharelatex' + +module.exports = CaptchaMiddleware = + validateCaptcha: (req, res, next) -> + if !Settings.recaptcha? + return next() + response = req.body['g-recaptcha-response'] + options = + form: + secret: Settings.recaptcha.secretKey + response: response + json: true + request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) -> + return next(error) if error? + if !body?.success + logger.warn {statusCode: response.statusCode, body: body}, 'failed recaptcha siteverify request' + return res.sendStatus 400 + else + return next() diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 55814980ad..28f0cbb5ee 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -33,7 +33,7 @@ module.exports = CollaboratorsInviteController = callback(null, userExists) else callback(null, true) - + _checkRateLimit: (user_id, callback = (error) ->) -> LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)-> return callback(err) if err? @@ -47,59 +47,43 @@ module.exports = CollaboratorsInviteController = throttle: collabLimit rateLimiter.addCount opts, callback - _validateCaptcha: (response, callback = (error, valid) ->) -> - if !Settings.recaptcha? - return callback(null, true) - options = - form: - secret: Settings.recaptcha.secretKey - response: response - json: true - request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) -> - return callback(error) if error? - return callback null, body?.success - inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email - CollaboratorsInviteController._validateCaptcha req.body['g-recaptcha-response'], (error, valid) -> + sendingUser = AuthenticationController.getSessionUser(req) + sendingUserId = sendingUser._id + if email == sendingUser.email + logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project" + return res.json {invite: null, error: 'cannot_invite_self'} + logger.log {projectId, email, sendingUserId}, "inviting to project" + LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => return next(error) if error? - if !valid - return res.sendStatus 400 - sendingUser = AuthenticationController.getSessionUser(req) - sendingUserId = sendingUser._id - if email == sendingUser.email - logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project" - return res.json {invite: null, error: 'cannot_invite_self'} - logger.log {projectId, email, sendingUserId}, "inviting to project" - LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => + if !allowed + logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" + return res.json {invite: null} + {email, privileges} = req.body + email = EmailHelper.parseEmail(email) + if !email? or email == "" + logger.log {projectId, email, sendingUserId}, "invalid email address" + return res.sendStatus(400) + CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) -> return next(error) if error? - if !allowed - logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" - return res.json {invite: null} - {email, privileges} = req.body - email = EmailHelper.parseEmail(email) - if !email? or email == "" - logger.log {projectId, email, sendingUserId}, "invalid email address" - return res.sendStatus(400) - CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) -> - return next(error) if error? - if !underRateLimit - return res.sendStatus(429) - CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> + if !underRateLimit + return res.sendStatus(429) + CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> + if err? + logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" + return next(err) + if !shouldAllowInvite + logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address" + return res.json {invite: null, error: 'cannot_invite_non_user'} + CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> if err? - logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" + logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) - if !shouldAllowInvite - logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address" - return res.json {invite: null, error: 'cannot_invite_non_user'} - CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> - if err? - logger.err {projectId, email, sendingUserId}, "error creating project invite" - return next(err) - logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) - return res.json {invite: invite} + logger.log {projectId, email, sendingUserId}, "invite created" + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) + return res.json {invite: invite} revokeInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 721e5a7b62..90fc704659 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -3,6 +3,7 @@ AuthenticationController = require('../Authentication/AuthenticationController') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') CollaboratorsInviteController = require('./CollaboratorsInviteController') RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') +CaptchaMiddleware = require '../Captcha/CaptchaMiddleware' module.exports = apply: (webRouter, apiRouter) -> @@ -32,6 +33,7 @@ module.exports = maxRequests: 100 timeInterval: 60 * 10 }), + CaptchaMiddleware.validateCaptcha, AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.inviteToProject diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index 0e6ae19ec2..f092f3a2dc 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -2,7 +2,7 @@ define [ "base" "libs/passfield" ], (App) -> - App.directive "asyncForm", ($http) -> + App.directive "asyncForm", ($http, validateCaptcha) -> return { controller: ['$scope', ($scope) -> @getEmail = () -> @@ -17,11 +17,23 @@ define [ element.on "submit", (e) -> e.preventDefault() + validateCaptchaIfEnabled (response) -> + submitRequest e, response + validateCaptchaIfEnabled = (callback = (response) ->) -> + if attrs.captcha? + validateCaptcha callback + else + callback() + + submitRequest = (e, grecaptchaResponse) -> formData = {} for data in element.serializeArray() formData[data.name] = data.value + if grecaptchaResponse? + formData['g-recaptcha-response'] = grecaptchaResponse + scope[attrs.name].inflight = true # for asyncForm prevent automatic redirect to /login if diff --git a/services/web/public/coffee/services/validateCaptcha.coffee b/services/web/public/coffee/services/validateCaptcha.coffee index 2f00c38f2a..7dfc038fbc 100644 --- a/services/web/public/coffee/services/validateCaptcha.coffee +++ b/services/web/public/coffee/services/validateCaptcha.coffee @@ -17,7 +17,7 @@ define [ _recaptchaCallbacks.push callback _recaptchaCallbacks.push reset if !recaptchaId? - el = $('.g-recaptcha')[0] + el = $('#recaptcha')[0] recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) grecaptcha.execute(recaptchaId) diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index e5452ddb7d..0adff748c0 100644 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -38,7 +38,6 @@ describe "CollaboratorsInviteController", -> '../Authentication/AuthenticationController': @AuthenticationController 'settings-sharelatex': @settings = {} "../../infrastructure/RateLimiter":@RateLimiter - 'request': @request = {} @res = new MockResponse() @req = new MockRequest() @@ -97,7 +96,6 @@ describe "CollaboratorsInviteController", -> @req.body = email: @targetEmail privileges: @privileges = "readAndWrite" - 'g-recaptcha-response': @grecaptchaResponse = 'grecaptcha response' @res.json = sinon.stub() @res.sendStatus = sinon.stub() @invite = { @@ -110,8 +108,6 @@ describe "CollaboratorsInviteController", -> } @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) - @CollaboratorsInviteController._validateCaptcha = sinon.stub() - @CollaboratorsInviteController._validateCaptcha.withArgs(@grecaptchaResponse).yields(null, true) @callback = sinon.stub() @next = sinon.stub() @@ -289,17 +285,6 @@ describe "CollaboratorsInviteController", -> it 'should not call emitToRoom', -> @EditorRealTimeController.emitToRoom.called.should.equal false - describe "when recaptcha is not valid", -> - beforeEach -> - @CollaboratorsInviteController._validateCaptcha = sinon.stub().yields(null, false) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it "should return 400", -> - @res.sendStatus.calledWith(400).should.equal true - - it "should not inviteToProject", -> - @CollaboratorsInviteHandler.inviteToProject.called.should.equal false - describe "viewInvite", -> beforeEach ->