From 83086e4a791bbfafb2189f764933ef2956adaa82 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 11:44:34 +0000 Subject: [PATCH 1/6] Add recaptch to share endpoint --- .../CollaboratorsInviteController.coffee | 77 +++++++++++-------- services/web/app/views/layout.pug | 3 + .../web/app/views/project/editor/share.pug | 7 ++ .../ShareProjectModalController.coffee | 29 ++++++- .../ide/share/services/projectInvites.coffee | 3 +- .../public/stylesheets/app/editor/share.less | 4 + .../CollaboratorsInviteControllerTests.coffee | 15 ++++ 7 files changed, 104 insertions(+), 34 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index e00f7807f5..55814980ad 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -11,6 +11,7 @@ NotificationsBuilder = require("../Notifications/NotificationsBuilder") AnalyticsManger = require("../Analytics/AnalyticsManager") AuthenticationController = require("../Authentication/AuthenticationController") rateLimiter = require("../../infrastructure/RateLimiter") +request = require 'request' module.exports = CollaboratorsInviteController = @@ -46,43 +47,59 @@ 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 - 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) => + CollaboratorsInviteController._validateCaptcha req.body['g-recaptcha-response'], (error, valid) -> 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) -> + 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) => return next(error) if error? - 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 !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 err? - logger.err {projectId, email, sendingUserId}, "error creating project invite" + logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" return next(err) - logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) - return res.json {invite: invite} + 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} revokeInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 31afc17d29..34e78a65f9 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -53,6 +53,9 @@ html(itemscope, itemtype='http://schema.org/Product') - else script(type='text/javascript'). window.ga = function() { console.log("would send to GA", arguments) }; + + - if (settings.recaptcha) + script(src="https://www.google.com/recaptcha/api.js") script(type="text/javascript"). window.csrfToken = "#{csrfToken}"; diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index 98568df122..8f5110fa6f 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -102,6 +102,13 @@ script(type='text/ng-template', id='shareProjectModalTemplate') i.fa.fa-times .row.invite-controls form(ng-show="canAddCollaborators") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) .small #{translate("share_with_your_collabs")} .form-group tags-input( diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 789f6272ca..70784e63cf 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -55,6 +55,25 @@ define [ getCurrentInviteEmails = () -> ($scope.project.invites || []).map (u) -> u.email + _recaptchaCallbacks = [] + onRecaptchaSubmit = (token) -> + for cb in _recaptchaCallbacks + cb(token) + _recaptchaCallbacks = [] + + recaptchaId = null + validateCaptcha = (callback = (response) ->) => + if !grecaptcha? + return callback() + reset = () -> + grecaptcha.reset() + _recaptchaCallbacks.push callback + _recaptchaCallbacks.push reset + if !recaptchaId? + el = $('.g-recaptcha')[0] + recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) + grecaptcha.execute(recaptchaId) + $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() return $scope.autocompleteContacts.filter (contact) -> @@ -100,7 +119,7 @@ define [ if email in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == email)?._id request = projectInvites.resendInvite(inviteId) else - request = projectInvites.sendInvite(email, $scope.inputs.privileges) + request = projectInvites.sendInvite(email, $scope.inputs.privileges, $scope.grecaptchaResponse) request .then (response) -> @@ -135,7 +154,9 @@ define [ else $scope.state.errorReason = null - $timeout addMembers, 50 # Give email list a chance to update + validateCaptcha (response) -> + $scope.grecaptchaResponse = response + $timeout addMembers, 50 # Give email list a chance to update $scope.removeMember = (member) -> $scope.state.error = null @@ -210,6 +231,8 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() + + App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) -> $scope.inputs = { privileges: "readAndWrite" @@ -244,4 +267,4 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() - ] + ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee index 4c0d30add6..84757915df 100644 --- a/services/web/public/coffee/ide/share/services/projectInvites.coffee +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -4,11 +4,12 @@ define [ App.factory "projectInvites", ["ide", "$http", (ide, $http) -> return { - sendInvite: (email, privileges) -> + sendInvite: (email, privileges, grecaptchaResponse) -> $http.post("/project/#{ide.project_id}/invite", { email: email privileges: privileges _csrf: window.csrfToken + 'g-recaptcha-response': grecaptchaResponse }) revokeInvite: (inviteId) -> diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index ba1e79f4b1..d0c9b5cde4 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -66,3 +66,7 @@ text-align: left; } } + +.grecaptcha-badge { + display: none; +} \ No newline at end of file diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 0adff748c0..e5452ddb7d 100644 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -38,6 +38,7 @@ describe "CollaboratorsInviteController", -> '../Authentication/AuthenticationController': @AuthenticationController 'settings-sharelatex': @settings = {} "../../infrastructure/RateLimiter":@RateLimiter + 'request': @request = {} @res = new MockResponse() @req = new MockRequest() @@ -96,6 +97,7 @@ describe "CollaboratorsInviteController", -> @req.body = email: @targetEmail privileges: @privileges = "readAndWrite" + 'g-recaptcha-response': @grecaptchaResponse = 'grecaptcha response' @res.json = sinon.stub() @res.sendStatus = sinon.stub() @invite = { @@ -108,6 +110,8 @@ 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() @@ -285,6 +289,17 @@ 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 -> From 0b03bbc7c307febf34393975e905596006ca10d7 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 12:04:33 +0000 Subject: [PATCH 2/6] Don't inject recaptcha element if recaptcha is not enabled --- services/web/app/views/project/editor/share.pug | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index 8f5110fa6f..0b56bbc4f5 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -102,13 +102,14 @@ script(type='text/ng-template', id='shareProjectModalTemplate') i.fa.fa-times .row.invite-controls form(ng-show="canAddCollaborators") - div( - id="recaptcha" - class="g-recaptcha" - data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" - ) + - if (settings.recaptcha) + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) .small #{translate("share_with_your_collabs")} .form-group tags-input( From 69499847e413f5dc646c9fed1c35153f296a9dd8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 12:32:43 +0000 Subject: [PATCH 3/6] Refactor front end code into validateCaptcha service --- services/web/app/views/layout.pug | 12 +++++++--- .../web/app/views/project/editor/share.pug | 8 ------- services/web/public/coffee/ide.coffee | 1 + .../ShareProjectModalController.coffee | 21 +--------------- services/web/public/coffee/main.coffee | 1 + .../coffee/services/validateCaptcha.coffee | 24 +++++++++++++++++++ 6 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 services/web/public/coffee/services/validateCaptcha.coffee diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 34e78a65f9..e12d7d6f81 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -53,9 +53,6 @@ html(itemscope, itemtype='http://schema.org/Product') - else script(type='text/javascript'). window.ga = function() { console.log("would send to GA", arguments) }; - - - if (settings.recaptcha) - script(src="https://www.google.com/recaptcha/api.js") script(type="text/javascript"). window.csrfToken = "#{csrfToken}"; @@ -100,6 +97,15 @@ html(itemscope, itemtype='http://schema.org/Product') } body + if(settings.recaptcha) + script(src="https://www.google.com/recaptcha/api.js?render=explicit") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) - if(typeof(suppressSystemMessages) == "undefined") .system-messages( diff --git a/services/web/app/views/project/editor/share.pug b/services/web/app/views/project/editor/share.pug index 0b56bbc4f5..98568df122 100644 --- a/services/web/app/views/project/editor/share.pug +++ b/services/web/app/views/project/editor/share.pug @@ -102,14 +102,6 @@ script(type='text/ng-template', id='shareProjectModalTemplate') i.fa.fa-times .row.invite-controls form(ng-show="canAddCollaborators") - - if (settings.recaptcha) - div( - id="recaptcha" - class="g-recaptcha" - data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" - ) .small #{translate("share_with_your_collabs")} .form-group tags-input( diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 728300456c..e31985ec8a 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -33,6 +33,7 @@ define [ "directives/expandableTextArea" "directives/videoPlayState" "services/queued-http" + "services/validateCaptcha" "filters/formatDate" "main/event" "main/account-upgrade" diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 70784e63cf..0c16dcbb39 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide) -> + App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide, validateCaptcha) -> $scope.inputs = { privileges: "readAndWrite" contacts: [] @@ -55,25 +55,6 @@ define [ getCurrentInviteEmails = () -> ($scope.project.invites || []).map (u) -> u.email - _recaptchaCallbacks = [] - onRecaptchaSubmit = (token) -> - for cb in _recaptchaCallbacks - cb(token) - _recaptchaCallbacks = [] - - recaptchaId = null - validateCaptcha = (callback = (response) ->) => - if !grecaptcha? - return callback() - reset = () -> - grecaptcha.reset() - _recaptchaCallbacks.push callback - _recaptchaCallbacks.push reset - if !recaptchaId? - el = $('.g-recaptcha')[0] - recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) - grecaptcha.execute(recaptchaId) - $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() return $scope.autocompleteContacts.filter (contact) -> diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 5ad6f37d34..e361f583c8 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -30,6 +30,7 @@ define [ "directives/maxHeight" "directives/creditCards" "services/queued-http" + "services/validateCaptcha" "filters/formatDate" "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> diff --git a/services/web/public/coffee/services/validateCaptcha.coffee b/services/web/public/coffee/services/validateCaptcha.coffee new file mode 100644 index 0000000000..2f00c38f2a --- /dev/null +++ b/services/web/public/coffee/services/validateCaptcha.coffee @@ -0,0 +1,24 @@ +define [ + "base" +], (App) -> + App.factory "validateCaptcha", () -> + _recaptchaCallbacks = [] + onRecaptchaSubmit = (token) -> + for cb in _recaptchaCallbacks + cb(token) + _recaptchaCallbacks = [] + + recaptchaId = null + validateCaptcha = (callback = (response) ->) => + if !grecaptcha? + return callback() + reset = () -> + grecaptcha.reset() + _recaptchaCallbacks.push callback + _recaptchaCallbacks.push reset + if !recaptchaId? + el = $('.g-recaptcha')[0] + recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) + grecaptcha.execute(recaptchaId) + + return validateCaptcha From 53dc8cddfcd6652fd6081667b6c2c4d7b9c8ff2f Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 12:58:55 +0000 Subject: [PATCH 4/6] Refactor captcha into middleware and angular service --- .../Features/Captcha/CaptchaMiddleware.coffee | 21 +++++ .../CollaboratorsInviteController.coffee | 78 ++++++++----------- .../Collaborators/CollaboratorsRouter.coffee | 2 + .../public/coffee/directives/asyncForm.coffee | 14 +++- .../coffee/services/validateCaptcha.coffee | 2 +- .../CollaboratorsInviteControllerTests.coffee | 15 ---- 6 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee 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 -> From f465a962d45b5faa81bef06685f136f2e92b74b2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 13:01:21 +0000 Subject: [PATCH 5/6] Put recaptcha css in a base location --- services/web/public/stylesheets/app/base.less | 4 ++++ services/web/public/stylesheets/app/editor/share.less | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/public/stylesheets/app/base.less b/services/web/public/stylesheets/app/base.less index 7f2d9a55ed..2f7b939fda 100644 --- a/services/web/public/stylesheets/app/base.less +++ b/services/web/public/stylesheets/app/base.less @@ -104,3 +104,7 @@ -ms-transform-origin: center bottom; transform-origin: center bottom; } + +.grecaptcha-badge { + display: none; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index d0c9b5cde4..ba1e79f4b1 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -66,7 +66,3 @@ text-align: left; } } - -.grecaptcha-badge { - display: none; -} \ No newline at end of file From de484e1a08cf039193af79a1f9ffb12ac72b7c59 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 11 Dec 2017 13:31:16 +0000 Subject: [PATCH 6/6] Remove unused reference to e --- services/web/public/coffee/directives/asyncForm.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index f092f3a2dc..15e3c9b3fe 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -18,7 +18,7 @@ define [ element.on "submit", (e) -> e.preventDefault() validateCaptchaIfEnabled (response) -> - submitRequest e, response + submitRequest response validateCaptchaIfEnabled = (callback = (response) ->) -> if attrs.captcha? @@ -26,7 +26,7 @@ define [ else callback() - submitRequest = (e, grecaptchaResponse) -> + submitRequest = (grecaptchaResponse) -> formData = {} for data in element.serializeArray() formData[data.name] = data.value