From 03555f0339091fe22c159bbb1f6e400332ee82df Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 20 Jan 2017 14:50:38 +0000 Subject: [PATCH 1/5] added chris and removed geri from pics --- services/web/public/img/about/chris.jpg | Bin 0 -> 7021 bytes services/web/public/img/about/geri.jpg | Bin 5579 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/web/public/img/about/chris.jpg delete mode 100644 services/web/public/img/about/geri.jpg diff --git a/services/web/public/img/about/chris.jpg b/services/web/public/img/about/chris.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d317be783a48c772b7e76b51f4f930ae855d535f GIT binary patch literal 7021 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 literal 0 HcmV?d00001 diff --git a/services/web/public/img/about/geri.jpg b/services/web/public/img/about/geri.jpg deleted file mode 100644 index 0de2f9a20e76f60d7993b467832d8c3ad98a368b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5579 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 DbA9q* From 6d35585847929930422b1d30c04d3d81e1b119cc Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 21 Jan 2017 12:43:06 +0000 Subject: [PATCH 2/5] limit number of invites each user can do done with the number of collaborators a user can add prevents notifications getting filled up as well --- .../CollaboratorsInviteController.coffee | 19 ++- .../CollaboratorsInviteControllerTests.coffee | 137 +++++++++--------- 2 files changed, 87 insertions(+), 69 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 460b62da1d..1fde81f5c9 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController") NotificationsBuilder = require("../Notifications/NotificationsBuilder") AnalyticsManger = require("../Analytics/AnalyticsManager") AuthenticationController = require("../Authentication/AuthenticationController") +rateLimiter = require("../../infrastructure/RateLimiter") module.exports = CollaboratorsInviteController = @@ -22,7 +23,7 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) - _checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) -> + _checkShouldInviteEmail: (sendingUser, 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) -> @@ -30,7 +31,19 @@ module.exports = CollaboratorsInviteController = userExists = user? and user?._id? callback(null, userExists) else - callback(null, true) + UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)-> + if err? + return callback(err) + collabLimit = user?.features?.collaborators || 1 + if collabLimit == -1 + collabLimit = 20 + collabLimit = collabLimit * 10 + opts = + endpointName: "invite_to_project" + timeInterval: 60 * 30 + subjectName: sendingUser._id + throttle: collabLimit + rateLimiter.addCount opts, callback inviteToProject: (req, res, next) -> projectId = req.params.Project_id @@ -51,7 +64,7 @@ module.exports = CollaboratorsInviteController = if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) - CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> + CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)-> if err? logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" return next(err) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index cf398e69da..bc1cb2e3b4 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -14,11 +14,20 @@ describe "CollaboratorsInviteController", -> @user = _id: 'id' @AnalyticsManger = recordEvent: sinon.stub() + @sendingUser = null @AuthenticationController = - getSessionUser: (req) => req.session.user + getSessionUser: (req) => + @sendingUser = req.session.user + return @sendingUser + + @RateLimiter = + addCount: sinon.stub + + @LimitationsManager = {} + @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: "../Project/ProjectGetter": @ProjectGetter = {} - '../Subscription/LimitationsManager' : @LimitationsManager = {} + '../Subscription/LimitationsManager' : @LimitationsManager '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} "./CollaboratorsHandler": @CollaboratorsHandler = {} "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} @@ -28,6 +37,7 @@ describe "CollaboratorsInviteController", -> "../Analytics/AnalyticsManager": @AnalyticsManger '../Authentication/AuthenticationController': @AuthenticationController 'settings-sharelatex': @settings = {} + "../../infrastructure/RateLimiter":@RateLimiter @res = new MockResponse() @req = new MockRequest() @@ -104,15 +114,10 @@ describe "CollaboratorsInviteController", -> describe 'when all goes well', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should produce json response', -> @res.json.callCount.should.equal 1 ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) @@ -122,8 +127,8 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @@ -136,22 +141,17 @@ describe "CollaboratorsInviteController", -> describe 'when the user is not allowed to add more collaborators', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should produce json response without an invite', -> @res.json.callCount.should.equal 1 ({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 + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -159,23 +159,18 @@ describe "CollaboratorsInviteController", -> describe 'when canAddXCollaborators produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @err = new Error('woops') @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) @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 not have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 0 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -183,16 +178,11 @@ describe "CollaboratorsInviteController", -> describe 'when inviteToProject produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @err = new Error('woops') @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) @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 @@ -202,8 +192,8 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @@ -212,22 +202,17 @@ describe "CollaboratorsInviteController", -> describe 'when _checkShouldInviteEmail disallows the invite', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, false) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, 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 + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -235,22 +220,17 @@ describe "CollaboratorsInviteController", -> describe 'when _checkShouldInviteEmail produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, new Error('woops')) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, 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 + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -260,14 +240,10 @@ describe "CollaboratorsInviteController", -> beforeEach -> @req.session.user = {_id: 'abc', email: 'me@example.com'} @req.body.email = 'me@example.com' - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() it 'should reject action, return json response with error code', -> @res.json.callCount.should.equal 1 @@ -277,7 +253,7 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.callCount.should.equal 0 it 'should not have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -702,13 +678,14 @@ describe "CollaboratorsInviteController", -> beforeEach -> @email = 'user@example.com' - @call = (callback) => - @CollaboratorsInviteController._checkShouldInviteEmail @email, callback + describe 'when we should be restricting to existing accounts', -> beforeEach -> @settings.restrictInvitesToExistingAccounts = true + @call = (callback) => + @CollaboratorsInviteController._checkShouldInviteEmail {}, @email, callback describe 'when user account is present', -> @@ -753,18 +730,46 @@ describe "CollaboratorsInviteController", -> expect(shouldAllow).to.equal undefined done() - describe 'when we should not be restricting', -> + describe 'when we should not be restricting on only registered users but do rate limit', -> beforeEach -> @settings.restrictInvitesToExistingAccounts = false + @sendingUser = + _id:"32312313" + features: + collaborators:17.8 + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @sendingUser) - it 'should callback with `true`', (done) -> - @call (err, shouldAllow) => - expect(err).to.equal null - expect(shouldAllow).to.equal true + it 'should callback with `true` when rate limit under', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.called.should.equal true + result.should.equal true done() - it 'should not have called getUser', (done) -> - @call (err, shouldAllow) => - @UserGetter.getUser.callCount.should.equal 0 + it 'should callback with `false` when rate limit hit', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.called.should.equal true + result.should.equal false done() + + it 'should call rate limiter with 10x the collaborators', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(178) + done() + + it 'should call rate limiter with 200 when collaborators is -1', (done) -> + @sendingUser.features.collaborators = -1 + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(200) + done() + + it 'should call rate limiter with 10 when user has no collaborators set', (done) -> + delete @sendingUser.features + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(10) + done() \ No newline at end of file From 74240e28c78cf6059fa2ce48db100c322c9adead Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 21 Jan 2017 12:44:09 +0000 Subject: [PATCH 3/5] rate limit via ip the number of invite to project requests --- .../Features/Collaborators/CollaboratorsRouter.coffee | 8 +++++++- .../coffee/Features/Security/RateLimiterMiddlewear.coffee | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 4c7cc8c76a..8b130d27db 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -24,7 +24,13 @@ module.exports = RateLimiterMiddlewear.rateLimit({ endpointName: "invite-to-project" params: ["Project_id"] - maxRequests: 200 + maxRequests: 100 + timeInterval: 60 * 10 + }), + RateLimiterMiddlewear.rateLimit({ + endpointName: "invite-to-project-ip" + ipOnly:true + maxRequests: 100 timeInterval: 60 * 10 }), AuthenticationController.requireLogin(), diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee index f486e94493..04b81581bf 100644 --- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee +++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee @@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear = user_id = AuthenticationController.getLoggedInUserId(req) || req.ip params = (opts.params or []).map (p) -> req.params[p] params.push user_id + subjectName = params.join(":") + if opts.ipOnly + subjectName = req.ip if !opts.endpointName? throw new Error("no endpointName provided") options = { endpointName: opts.endpointName timeInterval: opts.timeInterval or 60 - subjectName: params.join(":") + subjectName: subjectName throttle: opts.maxRequests or 6 } RateLimiter.addCount options, (error, canContinue)-> From 9153ffac41f240a901992ccb918843d393963014 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 21 Jan 2017 12:58:16 +0000 Subject: [PATCH 4/5] limit project name in email to 40 chars --- .../web/app/coffee/Features/Email/EmailBuilder.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 0a06a2a175..5360adb7a8 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -97,7 +97,7 @@ Thank you templates.projectInvite = - subject: _.template "<%= project.name %> - shared by <%= owner.email %>" + subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ @@ -111,16 +111,16 @@ Thank you """ compiledTemplate: (opts) -> SingleCTAEmailBody({ - title: "#{ opts.project.name } – shared by #{ opts.owner.email }" + title: "#{ opts.project.name.slice(0, 40) } – shared by #{ opts.owner.email }" greeting: "Hi," - message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + message: "#{ opts.owner.email } wants to share “#{ opts.project.name.slice(0, 40) }” with you." secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl gmailGoToAction: target: opts.inviteUrl name: "View project" - description: "Join #{ opts.project.name } at ShareLaTeX" + description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX" }) templates.completeJoinGroupAccount = From 2813b16ebfe7da9272b7c1fb979ca6cfa1a1a8e3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 23 Jan 2017 09:45:37 +0100 Subject: [PATCH 5/5] Use thread id for comment id --- .../web/public/coffee/ide/review-panel/RangesTracker.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 722eab1aa5..7a679bb6e3 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -107,7 +107,7 @@ load = (EventEmitter) -> addComment: (op, metadata) -> # TODO: Don't allow overlapping comments? @comments.push comment = { - id: @newId() + id: op.t or @newId() op: # Copy because we'll modify in place c: op.c p: op.p