Add option to restrict invites to existing user accounts.

This commit is contained in:
Shane Kilkelly 2016-12-20 09:54:42 +00:00
parent 3a8a12fcb3
commit 259c589076
6 changed files with 217 additions and 18 deletions

View file

@ -4,6 +4,7 @@ UserGetter = require "../User/UserGetter"
CollaboratorsHandler = require('./CollaboratorsHandler') CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
EmailHelper = require "../Helpers/EmailHelper" EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController") EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder") NotificationsBuilder = require("../Notifications/NotificationsBuilder")
@ -21,6 +22,16 @@ module.exports = CollaboratorsInviteController =
return next(err) return next(err)
res.json({invites: invites}) res.json({invites: invites})
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
if Settings.restrictInvitesToExistingAccounts == true
logger.log {email}, "checking if user exists with this email"
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
return callback(err) if err?
userExists = user? and user?._id?
callback(null, userExists)
else
callback(null, true)
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
@ -37,6 +48,13 @@ module.exports = CollaboratorsInviteController =
if !email? or email == "" if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address" logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400) return res.sendStatus(400)
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) -> CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
if err? if err?
logger.err {projectId, email, sendingUserId}, "error creating project invite" logger.err {projectId, email, sendingUserId}, "error creating project invite"

View file

@ -137,10 +137,15 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
p.small(ng-show="startedFreeTrial") p.small(ng-show="startedFreeTrial")
| #{translate("refresh_page_after_starting_free_trial")}. | #{translate("refresh_page_after_starting_free_trial")}.
.modal-footer .modal-footer.modal-footer-share
.modal-footer-left .modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight") i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} span.text-danger.error(ng-show="state.error")
span(ng-switch="state.errorReason")
span(ng-switch-when="cannot_invite_non_user")
| #{translate("cannot_invite_non_user")}
span(ng-switch-default)
| #{translate("generic_something_went_wrong")}
button.btn.btn-default( button.btn.btn-default(
ng-click="done()" ng-click="done()"
) #{translate("close")} ) #{translate("close")}

View file

@ -276,6 +276,10 @@ module.exports = settings =
# Cookie max age (in milliseconds). Set to false for a browser session. # Cookie max age (in milliseconds). Set to false for a browser session.
cookieSessionLength: 5 * 24 * 60 * 60 * 1000 # 5 days cookieSessionLength: 5 * 24 * 60 * 60 * 1000 # 5 days
# When true, only allow invites to be sent to email addresses that
# already have user accounts
restrictInvitesToExistingAccounts: false
# Should we allow access to any page without logging in? This includes # Should we allow access to any page without logging in? This includes
# public projects, /learn, /templates, about pages, etc. # public projects, /learn, /templates, about pages, etc.
allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false

View file

@ -8,6 +8,7 @@ define [
} }
$scope.state = { $scope.state = {
error: null error: null
errorReason: null
inflight: false inflight: false
startedFreeTrial: false startedFreeTrial: false
invites: [] invites: []
@ -69,7 +70,8 @@ define [
members = $scope.inputs.contacts members = $scope.inputs.contacts
$scope.inputs.contacts = [] $scope.inputs.contacts = []
$scope.state.error = null $scope.state.error = false
$scope.state.errorReason = null
$scope.state.inflight = true $scope.state.inflight = true
if !$scope.project.invites? if !$scope.project.invites?
@ -101,6 +103,11 @@ define [
request request
.success (data) -> .success (data) ->
if data.error
$scope.state.error = true
$scope.state.errorReason = "#{data.error}"
$scope.state.inflight = false
else
if data.invite if data.invite
invite = data.invite invite = data.invite
$scope.project.invites.push invite $scope.project.invites.push invite
@ -121,6 +128,7 @@ define [
.error () -> .error () ->
$scope.state.inflight = false $scope.state.inflight = false
$scope.state.error = true $scope.state.error = true
$scope.state.errorReason = null
$timeout addMembers, 50 # Give email list a chance to update $timeout addMembers, 50 # Give email list a chance to update

View file

@ -48,3 +48,9 @@
} }
} }
} }
.modal-footer-share {
.modal-footer-left {
max-width: 70%;
text-align: left;
}
}

View file

@ -27,6 +27,7 @@ describe "CollaboratorsInviteController", ->
"../Notifications/NotificationsBuilder": @NotificationsBuilder = {} "../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
"../Analytics/AnalyticsManager": @AnalyticsManger "../Analytics/AnalyticsManager": @AnalyticsManger
'../Authentication/AuthenticationController': @AuthenticationController '../Authentication/AuthenticationController': @AuthenticationController
'settings-sharelatex': @settings = {}
@res = new MockResponse() @res = new MockResponse()
@req = new MockRequest() @req = new MockRequest()
@ -103,9 +104,15 @@ describe "CollaboratorsInviteController", ->
describe 'when all goes well', -> describe 'when all goes well', ->
beforeEach -> beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, null, true)
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteController.inviteToProject @req, @res, @next @CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should produce json response', -> it 'should produce json response', ->
@res.json.callCount.should.equal 1 @res.json.callCount.should.equal 1
({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
@ -114,6 +121,10 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1 @LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 1
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should have called inviteToProject', -> it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
@ -125,37 +136,63 @@ describe "CollaboratorsInviteController", ->
describe 'when the user is not allowed to add more collaborators', -> describe 'when the user is not allowed to add more collaborators', ->
beforeEach -> beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, null, true)
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsInviteController.inviteToProject @req, @res, @next @CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should produce json response without an invite', -> it 'should produce json response without an invite', ->
@res.json.callCount.should.equal 1 @res.json.callCount.should.equal 1
({invite: null}).should.deep.equal(@res.json.firstCall.args[0]) ({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
it 'should not have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 0
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
it 'should not have called inviteToProject', -> it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when canAddXCollaborators produces an error', -> describe 'when canAddXCollaborators produces an error', ->
beforeEach -> beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, null, true)
@err = new Error('woops') @err = new Error('woops')
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next @CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should call next with an error', -> it 'should call next with an error', ->
@next.callCount.should.equal 1 @next.callCount.should.equal 1
@next.calledWith(@err).should.equal true @next.calledWith(@err).should.equal true
it 'should not have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 0
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
it 'should not have called inviteToProject', -> it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when inviteToProject produces an error', -> describe 'when inviteToProject produces an error', ->
beforeEach -> beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, null, true)
@err = new Error('woops') @err = new Error('woops')
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next @CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should call next with an error', -> it 'should call next with an error', ->
@next.callCount.should.equal 1 @next.callCount.should.equal 1
@next.calledWith(@err).should.equal true @next.calledWith(@err).should.equal true
@ -164,10 +201,60 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1 @LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 1
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should have called inviteToProject', -> it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
describe 'when _checkShouldInviteEmail disallows the invite', ->
beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, null, false)
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should produce json response with no invite, and an error property', ->
@res.json.callCount.should.equal 1
({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0])
it 'should have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 1
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when _checkShouldInviteEmail produces an error', ->
beforeEach ->
@_checkShouldInviteEmail = sinon.stub(
@CollaboratorsInviteController, '_checkShouldInviteEmail'
).callsArgWith(1, new Error('woops'))
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
afterEach ->
@_checkShouldInviteEmail.restore()
it 'should call next with an error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should have called _checkShouldInviteEmail', ->
@_checkShouldInviteEmail.callCount.should.equal 1
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe "viewInvite", -> describe "viewInvite", ->
beforeEach -> beforeEach ->
@ -579,3 +666,74 @@ describe "CollaboratorsInviteController", ->
it 'should have called acceptInvite', -> it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
describe '_checkShouldInviteEmail', ->
beforeEach ->
@email = 'user@example.com'
@call = (callback) =>
@CollaboratorsInviteController._checkShouldInviteEmail @email, callback
describe 'when we should be restricting to existing accounts', ->
beforeEach ->
@settings.restrictInvitesToExistingAccounts = true
describe 'when user account is present', ->
beforeEach ->
@user = {_id: ObjectId().toString()}
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
it 'should callback with `true`', (done) ->
@call (err, shouldAllow) =>
expect(err).to.equal null
expect(shouldAllow).to.equal true
done()
describe 'when user account is absent', ->
beforeEach ->
@user = null
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
it 'should callback with `false`', (done) ->
@call (err, shouldAllow) =>
expect(err).to.equal null
expect(shouldAllow).to.equal false
done()
it 'should have called getUser', (done) ->
@call (err, shouldAllow) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true
done()
describe 'when getUser produces an error', ->
beforeEach ->
@user = null
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
it 'should callback with an error', (done) ->
@call (err, shouldAllow) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
expect(shouldAllow).to.equal undefined
done()
describe 'when we should not be restricting', ->
beforeEach ->
@settings.restrictInvitesToExistingAccounts = false
it 'should callback with `true`', (done) ->
@call (err, shouldAllow) =>
expect(err).to.equal null
expect(shouldAllow).to.equal true
done()
it 'should not have called getUser', (done) ->
@call (err, shouldAllow) =>
@UserGetter.getUser.callCount.should.equal 0
done()