Refactor captcha into middleware and angular service

This commit is contained in:
James Allen 2017-12-11 12:58:55 +00:00
parent 69499847e4
commit 53dc8cddfc
6 changed files with 68 additions and 64 deletions

View file

@ -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()

View file

@ -47,59 +47,43 @@ module.exports = CollaboratorsInviteController =
throttle: collabLimit throttle: collabLimit
rateLimiter.addCount opts, callback 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) -> inviteToProject: (req, res, next) ->
projectId = req.params.Project_id projectId = req.params.Project_id
email = req.body.email 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? return next(error) if error?
if !valid if !allowed
return res.sendStatus 400 logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
sendingUser = AuthenticationController.getSessionUser(req) return res.json {invite: null}
sendingUserId = sendingUser._id {email, privileges} = req.body
if email == sendingUser.email email = EmailHelper.parseEmail(email)
logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project" if !email? or email == ""
return res.json {invite: null, error: 'cannot_invite_self'} logger.log {projectId, email, sendingUserId}, "invalid email address"
logger.log {projectId, email, sendingUserId}, "inviting to project" return res.sendStatus(400)
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
return next(error) if error? return next(error) if error?
if !allowed if !underRateLimit
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" return res.sendStatus(429)
return res.json {invite: null} CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
{email, privileges} = req.body if err?
email = EmailHelper.parseEmail(email) logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
if !email? or email == "" return next(err)
logger.log {projectId, email, sendingUserId}, "invalid email address" if !shouldAllowInvite
return res.sendStatus(400) logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) -> return res.json {invite: null, error: 'cannot_invite_non_user'}
return next(error) if error? CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
if !underRateLimit
return res.sendStatus(429)
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
if err? 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) return next(err)
if !shouldAllowInvite logger.log {projectId, email, sendingUserId}, "invite created"
logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address" EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
return res.json {invite: null, error: 'cannot_invite_non_user'} return res.json {invite: invite}
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) -> revokeInvite: (req, res, next) ->
projectId = req.params.Project_id projectId = req.params.Project_id

View file

@ -3,6 +3,7 @@ AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
CollaboratorsInviteController = require('./CollaboratorsInviteController') CollaboratorsInviteController = require('./CollaboratorsInviteController')
RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
CaptchaMiddleware = require '../Captcha/CaptchaMiddleware'
module.exports = module.exports =
apply: (webRouter, apiRouter) -> apply: (webRouter, apiRouter) ->
@ -32,6 +33,7 @@ module.exports =
maxRequests: 100 maxRequests: 100
timeInterval: 60 * 10 timeInterval: 60 * 10
}), }),
CaptchaMiddleware.validateCaptcha,
AuthenticationController.requireLogin(), AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject, AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.inviteToProject CollaboratorsInviteController.inviteToProject

View file

@ -2,7 +2,7 @@ define [
"base" "base"
"libs/passfield" "libs/passfield"
], (App) -> ], (App) ->
App.directive "asyncForm", ($http) -> App.directive "asyncForm", ($http, validateCaptcha) ->
return { return {
controller: ['$scope', ($scope) -> controller: ['$scope', ($scope) ->
@getEmail = () -> @getEmail = () ->
@ -17,11 +17,23 @@ define [
element.on "submit", (e) -> element.on "submit", (e) ->
e.preventDefault() e.preventDefault()
validateCaptchaIfEnabled (response) ->
submitRequest e, response
validateCaptchaIfEnabled = (callback = (response) ->) ->
if attrs.captcha?
validateCaptcha callback
else
callback()
submitRequest = (e, grecaptchaResponse) ->
formData = {} formData = {}
for data in element.serializeArray() for data in element.serializeArray()
formData[data.name] = data.value formData[data.name] = data.value
if grecaptchaResponse?
formData['g-recaptcha-response'] = grecaptchaResponse
scope[attrs.name].inflight = true scope[attrs.name].inflight = true
# for asyncForm prevent automatic redirect to /login if # for asyncForm prevent automatic redirect to /login if

View file

@ -17,7 +17,7 @@ define [
_recaptchaCallbacks.push callback _recaptchaCallbacks.push callback
_recaptchaCallbacks.push reset _recaptchaCallbacks.push reset
if !recaptchaId? if !recaptchaId?
el = $('.g-recaptcha')[0] el = $('#recaptcha')[0]
recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit})
grecaptcha.execute(recaptchaId) grecaptcha.execute(recaptchaId)

View file

@ -38,7 +38,6 @@ describe "CollaboratorsInviteController", ->
'../Authentication/AuthenticationController': @AuthenticationController '../Authentication/AuthenticationController': @AuthenticationController
'settings-sharelatex': @settings = {} 'settings-sharelatex': @settings = {}
"../../infrastructure/RateLimiter":@RateLimiter "../../infrastructure/RateLimiter":@RateLimiter
'request': @request = {}
@res = new MockResponse() @res = new MockResponse()
@req = new MockRequest() @req = new MockRequest()
@ -97,7 +96,6 @@ describe "CollaboratorsInviteController", ->
@req.body = @req.body =
email: @targetEmail email: @targetEmail
privileges: @privileges = "readAndWrite" privileges: @privileges = "readAndWrite"
'g-recaptcha-response': @grecaptchaResponse = 'grecaptcha response'
@res.json = sinon.stub() @res.json = sinon.stub()
@res.sendStatus = sinon.stub() @res.sendStatus = sinon.stub()
@invite = { @invite = {
@ -110,8 +108,6 @@ describe "CollaboratorsInviteController", ->
} }
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
@CollaboratorsInviteController._validateCaptcha = sinon.stub()
@CollaboratorsInviteController._validateCaptcha.withArgs(@grecaptchaResponse).yields(null, true)
@callback = sinon.stub() @callback = sinon.stub()
@next = sinon.stub() @next = sinon.stub()
@ -289,17 +285,6 @@ describe "CollaboratorsInviteController", ->
it 'should not call emitToRoom', -> it 'should not call emitToRoom', ->
@EditorRealTimeController.emitToRoom.called.should.equal false @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", -> describe "viewInvite", ->
beforeEach -> beforeEach ->