From 120a142733cbee88b464883c9add9697c71086fc Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 3 Mar 2016 16:12:48 +0000 Subject: [PATCH 01/48] Add in required abstracted functions to CollaboratorsHandler --- .../Collaborators/CollaboratorsHandler.coffee | 40 +++++++++ .../CollaboratorsHandlerTests.coffee | 87 +++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 4ab855af80..d574a0b263 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -6,8 +6,48 @@ logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +async = require "async" module.exports = CollaboratorsHandler = + getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + Project.findOne { _id: project_id }, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> + return callback(error) if error? + return callback null, null if !project? + members = [] + for member_id in project.readOnly_refs or [] + members.push { id: member_id, privilegeLevel: "readOnly" } + for member_id in project.collaberator_refs or [] + members.push { id: member_id, privilegeLevel: "readAndWrite" } + return callback null, members + + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + async.mapLimit (members or []), 3, + (member, cb) -> + UserGetter.getUser member.id, (error, user) -> + return cb(error) if error? + return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) + callback + + getMemberCount: (project_id, callback = (error, count) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, (members or []).length + + isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + for member in members or [] + if member.id.toString() == user_id.toString() + return callback null, true, member.privilegeLevel + return callback null, false, null + + getProjectsUserIsMemberOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> + Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> + Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> + callback(err, readAndWriteProjects, readOnlyProjects) + removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" conditions = _id:project_id diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 1fd3a0c2d5..631559b84e 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -22,6 +22,93 @@ describe "CollaboratorsHandler", -> @adding_user_id = "adding-user-id" @email = "joe@sharelatex.com" @callback = sinon.stub() + + describe "getMemberIdsWithPrivilegeLevels", -> + beforeEach -> + @Project.findOne = sinon.stub() + @Project.findOne.withArgs({_id: @project_id}, {collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] + collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] + }) + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback + + it "should return an array of member ids with their privilege levels", -> + @callback + .calledWith(null, [ + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "getMembersWithPrivilegeLevels", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + ]) + @UserGetter.getUser = sinon.stub() + @UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) + @UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" }) + @UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" }) + @UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) + @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback + + it "should return an array of members with their privilege levels", -> + @callback + .calledWith(undefined, [ + { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } + { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } + { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" } + { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "isUserMemberOfProject", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + + describe "when user is a member of the project", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "not-the-user", privilegeLevel: "readOnly" } + { id: @user_id, privilegeLevel: "readAndWrite" } + ]) + @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback + + it "should return true and the privilegeLevel", -> + @callback + .calledWith(null, true, "readAndWrite") + .should.equal true + + describe "when user is not a member of the project", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "not-the-user", privilegeLevel: "readOnly" } + ]) + @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback + + it "should return false", -> + @callback + .calledWith(null, false, null) + .should.equal true + + describe "getProjectsUserIsMemberOf", -> + beforeEach -> + @fields = "mock fields" + @Project.find = sinon.stub() + @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) + @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) + @CollaboratorHandler.getProjectsUserIsMemberOf @user_id, @fields, @callback + + it "should call the callback with the projects", -> + @callback + .calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"]) + .should.equal true describe "removeUserFromProject", -> beforeEach -> From 1a689aa1fd63ed7204dfb80af3c714ec91b2d204 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 3 Mar 2016 17:19:03 +0000 Subject: [PATCH 02/48] Move findAllUsersProjects from Project to ProjectGetter --- .../Features/Project/ProjectController.coffee | 3 ++- .../Features/Project/ProjectGetter.coffee | 9 +++++++++ .../Features/Project/ProjectLocator.coffee | 4 +++- services/web/app/coffee/models/Project.coffee | 6 ------ .../Project/ProjectControllerTests.coffee | 6 ++++-- .../coffee/Project/ProjectGetterTests.coffee | 17 ++++++++++++++++- .../coffee/Project/ProjectLocatorTests.coffee | 9 +++++---- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 7b36465f9f..b30c2b8f91 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -17,6 +17,7 @@ SecurityManager = require("../../managers/SecurityManager") fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") +ProjectGetter = require("./ProjectGetter") module.exports = ProjectController = @@ -129,7 +130,7 @@ module.exports = ProjectController = notifications: (cb)-> NotificationsHandler.getUserNotifications user_id, cb projects: (cb)-> - Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb + ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb hasSubscription: (cb)-> LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb user: (cb) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 129ff831dc..826ea4116c 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -2,6 +2,8 @@ mongojs = require("../../infrastructure/mongojs") db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" +Project = require("../../models/Project").Project +CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 @@ -27,6 +29,13 @@ module.exports = ProjectGetter = else if query instanceof ObjectId query = _id: query db.projects.findOne query, projection, callback + + findAllUsersProjects: (user_id, fields, callback = (error, ownedProjects, readAndWriteProjects, readOnlyProjects) ->) -> + Project.find {owner_ref: user_id}, fields, (error, projects) -> + return callback(error) if error? + CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> + return callback(error) if error? + callback null, projects, readAndWriteProjects, readOnlyProjects populateProjectWithUsers: (project, callback=(error, project) ->) -> # eventually this should be in a UserGetter.getUser module diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index fe60e0ba3e..0302bdc11b 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -3,6 +3,7 @@ Errors = require "../../errors" _ = require('underscore') logger = require('logger-sharelatex') async = require('async') +ProjectGetter = require "./ProjectGetter" module.exports = findElement: (options, _callback = (err, element, path, parentFolder)->)-> @@ -126,7 +127,8 @@ module.exports = async.waterfall jobs, callback findUsersProjectByName: (user_id, projectName, callback)-> - Project.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])-> + ProjectGetter.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])-> + return callback(error) if error? projects = projects.concat(collabertions) projectName = projectName.toLowerCase() project = _.find projects, (project)-> diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 77fbdaf1d4..3a391adfc0 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -60,12 +60,6 @@ ProjectSchema.statics.findPopulatedById = (project_id, callback)-> logger.log project_id:project_id, "finished findPopulatedById" callback(null, projects[0]) -ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)-> - this.find {owner_ref:user_id}, requiredFields, (err, projects)=> - this.find {collaberator_refs:user_id}, requiredFields, (err, collabertions)=> - this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=> - callback(err, projects, collabertions, readOnlyProjects) - sanitizeTypeOfElement = (elementType)-> lastChar = elementType.slice -1 if lastChar != "s" diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 3ecb31ba68..186c2e9836 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -36,7 +36,6 @@ describe "ProjectController", -> @NotificationsHandler = getUserNotifications: sinon.stub() @ProjectModel = - findAllUsersProjects: sinon.stub() findPopulatedById: sinon.stub() @UserModel = findById: sinon.stub() @@ -50,6 +49,8 @@ describe "ProjectController", -> markAsOpened: sinon.stub() @ReferencesSearchHandler = indexProjectReferences: sinon.stub() + @ProjectGetter = + findAllUsersProjects: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -69,6 +70,7 @@ describe "ProjectController", -> "../InactiveData/InactiveProjectManager":@InactiveProjectManager "./ProjectUpdateHandler":@ProjectUpdateHandler "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler + "./ProjectGetter": @ProjectGetter @user = _id:"!£123213kjljkl" @@ -220,7 +222,7 @@ describe "ProjectController", -> @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false) @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) - @ProjectModel.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly) + @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly) it "should render the project/list page", (done)-> @res.render = (pageName, opts)=> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index b8f1480be6..a107031c0b 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -16,6 +16,8 @@ describe "ProjectGetter", -> projects: {} users: {} ObjectId: ObjectId + "../../models/Project": Project: @Project = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} describe "getProjectWithoutDocLines", -> beforeEach -> @@ -111,4 +113,17 @@ describe "ProjectGetter", -> it "should call the callback", -> assert.deepEqual @callback.args[0][1], @project - + + describe "findAllUsersProjects", -> + beforeEach -> + @fields = {"mock": "fields"} + @Project.find = sinon.stub() + @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-projects"]) + @CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() + @CollaboratorsHandler.getProjectsUserIsMemberOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) + @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback + + it "should call the callback with all the projects", -> + @callback + .calledWith(null, ["mock-owned-projects"], ["mock-rw-projects"], ["mock-ro-projects"]) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index 5c36c18493..2bbcefd340 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -30,7 +30,7 @@ project.rootFolder[0] = rootFolder project.rootDoc_id = rootDoc._id -describe 'project model', -> +describe 'ProjectLocator', -> beforeEach -> Project.getProject = (project_id, fields, callback)=> @@ -41,6 +41,7 @@ describe 'project model', -> @locator = SandboxedModule.require modulePath, requires: '../../models/Project':{Project:Project} '../../models/User':{User:@User} + './ProjectGetter': @ProjectGetter = {} 'logger-sharelatex': log:-> err:-> @@ -298,7 +299,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis"} projects = [{name:"notThis"}, {name:"wellll"}, stubbedProject, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project.should.equal stubbedProject done() @@ -307,7 +308,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis", _id:12331321} projects = [{name:"notThis"}, {name:"wellll"}, {name:"findThis",archived:true}, stubbedProject, {name:"findThis",archived:true}, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project._id.should.equal stubbedProject._id done() @@ -316,7 +317,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis"} projects = [{name:"notThis"}, {name:"wellll"}, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects, [stubbedProject]) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects, [stubbedProject]) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project.should.equal stubbedProject done() From 6d93076d515bd83f8b13b80b5838bf5bbdfca657 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 12:02:48 +0000 Subject: [PATCH 03/48] Refactor getCollaborators http method to use CollaboratorsHandler --- .../CollaboratorsController.coffee | 27 ++-- .../Collaborators/CollaboratorsHandler.coffee | 8 +- .../Features/Project/ProjectGetter.coffee | 2 +- .../CollaboratorsControllerTests.coffee | 145 +++++------------- .../CollaboratorsHandlerTests.coffee | 4 +- 5 files changed, 63 insertions(+), 123 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 788b782eb2..82912e1b6b 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -8,13 +8,12 @@ mimelib = require("mimelib") module.exports = CollaboratorsController = getCollaborators: (req, res, next = (error) ->) -> - ProjectGetter.getProject req.params.Project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true}, (error, project) -> + project_id = req.params.Project_id + CollaboratorsHandler.getMembersWithPrivilegeLevels project_id, (error, members) -> return next(error) if error? - ProjectGetter.populateProjectWithUsers project, (error, project) -> + CollaboratorsController._formatCollaborators members, (error, collaborators) -> return next(error) if error? - CollaboratorsController._formatCollaborators project, (error, collaborators) -> - return next(error) if error? - res.send(JSON.stringify(collaborators)) + res.json(collaborators) addUserToProject: (req, res, next) -> project_id = req.params.Project_id @@ -59,7 +58,7 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - _formatCollaborators: (project, callback = (error, collaborators) ->) -> + _formatCollaborators: (members, callback = (error, collaborators) ->) -> collaborators = [] pushCollaborator = (user, permissions, owner) -> @@ -71,16 +70,14 @@ module.exports = CollaboratorsController = permissions: permissions owner: owner } - - if project.owner_ref? - pushCollaborator(project.owner_ref, ["read", "write", "admin"], true) - - if project.collaberator_refs? and project.collaberator_refs.length > 0 - for user in project.collaberator_refs + + for member in members + {user, privilegeLevel} = member + if privilegeLevel == "admin" + pushCollaborator(user, ["read", "write", "admin"], true) + else if privilegeLevel == "readAndWrite" pushCollaborator(user, ["read", "write"], false) - - if project.readOnly_refs? and project.readOnly_refs.length > 0 - for user in project.readOnly_refs + else pushCollaborator(user, ["read"], false) callback null, collaborators diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index d574a0b263..d1c355b79a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -10,10 +10,11 @@ async = require "async" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - Project.findOne { _id: project_id }, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> + Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? return callback null, null if !project? members = [] + members.push { id: project.owner_ref.toString(), privilegeLevel: "admin" } for member_id in project.readOnly_refs or [] members.push { id: member_id, privilegeLevel: "readOnly" } for member_id in project.collaberator_refs or [] @@ -34,6 +35,11 @@ module.exports = CollaboratorsHandler = CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, (members or []).length + + getCollaboratorCount: (project_id, callback = (error, count) ->) -> + CollaboratorsHandler.getMemberCount project_id, (error, count) -> + return callback(error) if error? + return callback null, count - 1 # Don't count project owner isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 826ea4116c..13fa409e99 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -3,7 +3,6 @@ db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" Project = require("../../models/Project").Project -CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 @@ -31,6 +30,7 @@ module.exports = ProjectGetter = db.projects.findOne query, projection, callback findAllUsersProjects: (user_id, fields, callback = (error, ownedProjects, readAndWriteProjects, readOnlyProjects) ->) -> + CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" Project.find {owner_ref: user_id}, fields, (error, projects) -> return callback(error) if error? CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 6930707249..84b0a250d3 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -26,32 +26,49 @@ describe "CollaboratorsController", -> describe "getCollaborators", -> beforeEach -> - @project = - _id: @project_id = "project-id-123" - @collaborators = ["array of collaborators"] - @req.params = Project_id: @project_id - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) - @CollaboratorsController._formatCollaborators = sinon.stub().callsArgWith(1, null, @collaborators) + @req.params = + Project_id: @project_id + @members = [ + { + user: { _id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin", foo: "bar" } + privilegeLevel: "admin" + }, + { + user: { _id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write", foo: "bar" } + privilegeLevel: "readAndWrite" + }, + { + user: { _id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read", foo: "bar" } + privilegeLevel: "readOnly" + } + ] + @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub() + @CollaboratorsHandler.getMembersWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, @members) + @res.json = sinon.stub() @CollaboratorsController.getCollaborators(@req, @res) - it "should get the project", -> - @ProjectGetter.getProject - .calledWith(@project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true }) - .should.equal true - - it "should populate the users in the project", -> - @ProjectGetter.populateProjectWithUsers - .calledWith(@project) - .should.equal true - - it "should format the collaborators", -> - @CollaboratorsController._formatCollaborators - .calledWith(@project) - .should.equal true - it "should return the formatted collaborators", -> - @res.body.should.equal JSON.stringify(@collaborators) + @res.json + .calledWith([ + { + id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin" + permissions: ["read", "write", "admin"] + owner: true + } + { + id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write" + permissions: ["read", "write"] + owner: false + } + { + id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read" + permissions: ["read"] + owner: false + } + ]) + .should.equal true describe "addUserToProject", -> beforeEach -> @@ -179,85 +196,3 @@ describe "CollaboratorsController", -> it "should return a success code", -> @res.sendStatus.calledWith(204).should.equal true - describe "_formatCollaborators", -> - beforeEach -> - @owner = - _id: ObjectId() - first_name: "Lenny" - last_name: "Lion" - email: "test@sharelatex.com" - hashed_password: "password" # should not be included - - describe "formatting the owner", -> - beforeEach -> - @project = - owner_ref: @owner - collaberator_refs: [] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the owner with read, write and admin permissions", -> - @formattedOwner = @callback.args[0][1][0] - expect(@formattedOwner).to.deep.equal { - id: @owner._id.toString() - first_name: @owner.first_name - last_name: @owner.last_name - email: @owner.email - permissions: ["read", "write", "admin"] - owner: true - } - - describe "formatting a collaborator with write access", -> - beforeEach -> - @collaborator = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - hashed_password: "password" # should not be included - - @project = - owner_ref: @owner - collaberator_refs: [ @collaborator ] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the collaborator with read and write permissions", -> - @formattedCollaborator = @callback.args[0][1][1] - expect(@formattedCollaborator).to.deep.equal { - id: @collaborator._id.toString() - first_name: @collaborator.first_name - last_name: @collaborator.last_name - email: @collaborator.email - permissions: ["read", "write"] - owner: false - } - - describe "formatting a collaborator with read only access", -> - beforeEach -> - @collaborator = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - hashed_password: "password" # should not be included - - @project = - owner_ref: @owner - collaberator_refs: [] - readOnly_refs: [ @collaborator ] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the collaborator with read permissions", -> - @formattedCollaborator = @callback.args[0][1][1] - expect(@formattedCollaborator).to.deep.equal { - id: @collaborator._id.toString() - first_name: @collaborator.first_name - last_name: @collaborator.last_name - email: @collaborator.email - permissions: ["read"] - owner: false - } - - - - - diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 631559b84e..f97364b756 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -26,7 +26,8 @@ describe "CollaboratorsHandler", -> describe "getMemberIdsWithPrivilegeLevels", -> beforeEach -> @Project.findOne = sinon.stub() - @Project.findOne.withArgs({_id: @project_id}, {collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + owner_ref: [ "owner-ref" ] readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] }) @@ -35,6 +36,7 @@ describe "CollaboratorsHandler", -> it "should return an array of member ids with their privilege levels", -> @callback .calledWith(null, [ + { id: "owner-ref", privilegeLevel: "admin" } { id: "read-only-ref-1", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } From 2ba2b72fd1232f01ab4b7ee589000bef1a597dcb Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 12:27:40 +0000 Subject: [PATCH 04/48] Refactor ProjectDeleter to use CollaboratorHandler --- .../Collaborators/CollaboratorsHandler.coffee | 5 +++++ .../coffee/Features/Project/ProjectDeleter.coffee | 13 ++++--------- .../Collaborators/CollaboratorsHandlerTests.coffee | 13 +++++++++++++ .../coffee/Project/ProjectDeleterTests.coffee | 11 +++++------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index d1c355b79a..ef2f96ea3d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -21,6 +21,11 @@ module.exports = CollaboratorsHandler = members.push { id: member_id, privilegeLevel: "readAndWrite" } return callback null, members + getMemberIds: (project_id, callback = (error, member_ids) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, members.map (m) -> m.id + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 6fdba733e7..3c11a777d6 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -4,6 +4,7 @@ documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') tagsHandler = require("../Tags/TagsHandler") async = require("async") FileStoreHandler = require("../FileStore/FileStoreHandler") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = ProjectDeleter = @@ -43,16 +44,10 @@ module.exports = ProjectDeleter = (cb)-> documentUpdaterHandler.flushProjectToMongoAndDelete project_id, cb (cb)-> - tagsHandler.removeProjectFromAllTags project.owner_ref, project_id, (err)-> + CollaboratorsHandler.getMemberIds project_id, (error, member_ids = []) -> + for member_id in member_ids + tagsHandler.removeProjectFromAllTags member_id, project_id, (err)-> cb() #doesn't matter if this fails or the order it happens in - (cb)-> - project.collaberator_refs.forEach (collaberator_ref)-> - tagsHandler.removeProjectFromAllTags collaberator_ref, project_id, -> - cb() - (cb)-> - project.readOnly_refs.forEach (readOnly_ref)-> - tagsHandler.removeProjectFromAllTags readOnly_ref, project_id, -> - cb() (cb)-> Project.update {_id:project_id}, { $set: { archived: true }}, cb ], (err)-> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f97364b756..f0fd8db4a9 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -44,6 +44,19 @@ describe "CollaboratorsHandler", -> ]) .should.equal true + describe "getMemberIds", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, [{id: "member-id-1"}, {id: "member-id-2"}]) + @CollaboratorHandler.getMemberIds @project_id, @callback + + it "should return the ids", -> + @callback + .calledWith(null, ["member-id-1", "member-id-2"]) + .should.equal true + describe "getMembersWithPrivilegeLevels", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee index 13f5d2a694..7ed1dac666 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee @@ -31,6 +31,7 @@ describe 'ProjectDeleter', -> '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler "../Tags/TagsHandler":@TagsHandler "../FileStore/FileStoreHandler": @FileStoreHandler = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} 'logger-sharelatex': log:-> @@ -89,6 +90,8 @@ describe 'ProjectDeleter', -> describe "archiveProject", -> beforeEach -> + @CollaboratorsHandler.getMemberIds = sinon.stub() + @CollaboratorsHandler.getMemberIds.withArgs(@project_id).yields(null, ["member-id-1", "member-id-2"]) @Project.update.callsArgWith(2) it "should flushProjectToMongoAndDelete in doc updater", (done)-> @@ -107,12 +110,8 @@ describe 'ProjectDeleter', -> it "should removeProjectFromAllTags", (done)-> @deleter.archiveProject @project_id, => - @TagsHandler.removeProjectFromAllTags.calledWith(@project.owner_ref, @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.collaberator_refs[0], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.collaberator_refs[1], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.readOnly_refs[0], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.readOnly_refs[1], @project_id).should.equal true - + @TagsHandler.removeProjectFromAllTags.calledWith("member-id-1", @project_id).should.equal true + @TagsHandler.removeProjectFromAllTags.calledWith("member-id-2", @project_id).should.equal true done() describe "restoreProject", -> From bedc8a049210b9b51732f57665fa06f4670731ca Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 15:25:10 +0000 Subject: [PATCH 05/48] Remove ProjectGetter.populateProjectWithUsers --- .../Editor/EditorHttpController.coffee | 5 +- .../Project/ProjectEditorHandler.coffee | 69 +++++++++--------- .../Features/Project/ProjectGetter.coffee | 40 ---------- .../Editor/EditorHttpControllerTests.coffee | 8 +- .../Project/ProjectEditorHandlerTests.coffee | 73 +++++++++---------- .../coffee/Project/ProjectGetterTests.coffee | 35 --------- 6 files changed, 76 insertions(+), 154 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 2619316e6d..18e8f7d308 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -8,6 +8,7 @@ UserGetter = require('../User/UserGetter') AuthorizationManager = require("../Security/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = EditorHttpController = joinProject: (req, res, next) -> @@ -29,7 +30,7 @@ module.exports = EditorHttpController = ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> return callback(error) if error? return callback(new Error("not found")) if !project? - ProjectGetter.populateProjectWithUsers project, (error, project) -> + CollaboratorsHandler.getMembersWithPrivilegeLevels project, (error, members) -> return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? @@ -39,7 +40,7 @@ module.exports = EditorHttpController = callback null, null, false else callback(null, - ProjectEditorHandler.buildProjectModelView(project), + ProjectEditorHandler.buildProjectModelView(project, members), privilegeLevel ) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index f12a7d548e..5b3143d765 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,11 +1,7 @@ _ = require("underscore") module.exports = ProjectEditorHandler = - buildProjectModelView: (project, options) -> - options ||= {} - if !options.includeUsers? - options.includeUsers = true - + buildProjectModelView: (project, members) -> result = _id : project._id name : project.name @@ -18,40 +14,41 @@ module.exports = ProjectEditorHandler = spellCheckLanguage: project.spellCheckLanguage deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs + members: [] - if options.includeUsers - result.features = - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false + result.features = # defaults + collaborators: -1 # Infinite + versioning: false + dropbox:false + compileTimeout: 60 + compileGroup:"standard" + templates: false + references: false + + owner = null + for member in members + if member.privilegeLevel == "admin" + owner = member.user + else + result.members.push @buildUserModelView member.user, member.privilegeLevel + result.owner = @buildUserModelView owner, "owner" - if project.owner_ref.features? - if project.owner_ref.features.collaborators? - result.features.collaborators = project.owner_ref.features.collaborators - if project.owner_ref.features.versioning? - result.features.versioning = project.owner_ref.features.versioning - if project.owner_ref.features.dropbox? - result.features.dropbox = project.owner_ref.features.dropbox - if project.owner_ref.features.compileTimeout? - result.features.compileTimeout = project.owner_ref.features.compileTimeout - if project.owner_ref.features.compileGroup? - result.features.compileGroup = project.owner_ref.features.compileGroup - if project.owner_ref.features.templates? - result.features.templates = project.owner_ref.features.templates - if project.owner_ref.features.references? - result.features.references = project.owner_ref.features.references + if owner?.features? + if owner.features.collaborators? + result.features.collaborators = owner.features.collaborators + if owner.features.versioning? + result.features.versioning = owner.features.versioning + if owner.features.dropbox? + result.features.dropbox = owner.features.dropbox + if owner.features.compileTimeout? + result.features.compileTimeout = owner.features.compileTimeout + if owner.features.compileGroup? + result.features.compileGroup = owner.features.compileGroup + if owner.features.templates? + result.features.templates = owner.features.templates + if owner.features.references? + result.features.references = owner.features.references - - result.owner = @buildUserModelView project.owner_ref, "owner" - result.members = [] - for ref in project.readOnly_refs - result.members.push @buildUserModelView ref, "readOnly" - for ref in project.collaberator_refs - result.members.push @buildUserModelView ref, "readAndWrite" return result buildUserModelView: (user, privileges) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 13fa409e99..7d9fd13bb6 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -36,43 +36,3 @@ module.exports = ProjectGetter = CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> return callback(error) if error? callback null, projects, readAndWriteProjects, readOnlyProjects - - populateProjectWithUsers: (project, callback=(error, project) ->) -> - # eventually this should be in a UserGetter.getUser module - getUser = (user_id, callback=(error, user) ->) -> - unless user_id instanceof ObjectId - user_id = ObjectId(user_id) - db.users.find _id: user_id, (error, users = []) -> - callback error, users[0] - - jobs = [] - jobs.push (callback) -> - getUser project.owner_ref, (error, user) -> - return callback(error) if error? - if user? - project.owner_ref = user - callback null, project - - readOnly_refs = project.readOnly_refs - project.readOnly_refs = [] - for readOnly_ref in readOnly_refs - do (readOnly_ref) -> - jobs.push (callback) -> - getUser readOnly_ref, (error, user) -> - return callback(error) if error? - if user? - project.readOnly_refs.push user - callback null, project - - collaberator_refs = project.collaberator_refs - project.collaberator_refs = [] - for collaberator_ref in collaberator_refs - do (collaberator_ref) -> - jobs.push (callback) -> - getUser collaberator_ref, (error, user) -> - return callback(error) if error? - if user? - project.collaberator_refs.push user - callback null, project - - async.parallelLimit jobs, 3, (error) -> callback error, project diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index ab003208c2..21c8a195e4 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -16,6 +16,7 @@ describe "EditorHttpController", -> "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } "./EditorController": @EditorController = {} '../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} @project_id = "mock-project-id" @doc_id = "mock-doc-id" @@ -85,13 +86,14 @@ describe "EditorHttpController", -> @user = _id: @user_id = "user-id" projects: {} + @members = ["members", "mock"] @projectModelView = _id: @project_id owner:{_id:"something"} view: true @ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView) @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) + @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) describe "when authorized", -> @@ -105,8 +107,8 @@ describe "EditorHttpController", -> .calledWith(@project_id) .should.equal true - it "should populate the user references in the project", -> - @ProjectGetter.populateProjectWithUsers + it "should get the list of users in the project", -> + @CollaboratorsHandler.getMembersWithPrivilegeLevels .calledWith(@project) .should.equal true diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 52c871669a..7d71f6c89f 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -38,33 +38,41 @@ describe "ProjectEditorHandler", -> folders : [] }] }] - owner_ref : - _id: "owner-id" - first_name : "Owner" - last_name : "ShareLaTeX" - email : "owner@sharelatex.com" - readOnly_refs: [{ - _id: "read-only-id" - first_name : "Read" - last_name : "Only" - email : "read-only@sharelatex.com" - }] - collaberator_refs: [{ - _id: "read-write-id" - first_name : "Read" - last_name : "Write" - email : "read-write@sharelatex.com" - }] deletedDocs: [{ _id: "deleted-doc-id" name: "main.tex" }] + @members = [{ + user: @owner = { + _id: "owner-id" + first_name : "Owner" + last_name : "ShareLaTeX" + email : "owner@sharelatex.com" + }, + privilegeLevel: "admin" + },{ + user: { + _id: "read-only-id" + first_name : "Read" + last_name : "Only" + email : "read-only@sharelatex.com" + }, + privilegeLevel: "readOnly" + },{ + user: { + _id: "read-write-id" + first_name : "Read" + last_name : "Write" + email : "read-write@sharelatex.com" + }, + privilegeLevel: "readAndWrite" + }] @handler = SandboxedModule.require modulePath describe "buildProjectModelView", -> describe "with owner and members included", -> beforeEach -> - @result = @handler.buildProjectModelView @project + @result = @handler.buildProjectModelView @project, @members it "should include the id", -> should.exist @result._id @@ -140,41 +148,30 @@ describe "ProjectEditorHandler", -> it "should set the deletedByExternalDataSource flag to false when it is not there", -> delete @project.deletedByExternalDataSource - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal false it "should set the deletedByExternalDataSource flag to false when it is false", -> - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal false it "should set the deletedByExternalDataSource flag to true when it is true", -> @project.deletedByExternalDataSource = true - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal true describe "features", -> beforeEach -> - @project.owner_ref.features = + @owner.features = versioning: true collaborators: 3 compileGroup:"priority" compileTimeout: 96 - @result = @handler.buildProjectModelView @project + @result = @handler.buildProjectModelView @project, @members it "should copy the owner features to the project", -> - @result.features.versioning.should.equal @project.owner_ref.features.versioning - @result.features.collaborators.should.equal @project.owner_ref.features.collaborators - @result.features.compileGroup.should.equal @project.owner_ref.features.compileGroup - @result.features.compileTimeout.should.equal @project.owner_ref.features.compileTimeout + @result.features.versioning.should.equal @owner.features.versioning + @result.features.collaborators.should.equal @owner.features.collaborators + @result.features.compileGroup.should.equal @owner.features.compileGroup + @result.features.compileTimeout.should.equal @owner.features.compileTimeout - - describe "without owners and members", -> - beforeEach -> - @result = @handler.buildProjectModelView @project, includeUsers: false - - it "should not include the owner", -> - should.not.exist @result.owner - - it "should not include the members", -> - should.not.exist @result.members - diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index a107031c0b..da9b9765e3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -79,41 +79,6 @@ describe "ProjectGetter", -> it "should call the callback with the project", -> @callback.calledWith(null, @project).should.equal true - - describe "populateProjectWithUsers", -> - beforeEach -> - @users = [] - @user_lookup = {} - for i in [0..4] - @users[i] = _id: ObjectId.createPk() - @user_lookup[@users[i]._id.toString()] = @users[i] - @project = - _id: ObjectId.createPk() - owner_ref: @users[0]._id - readOnly_refs: [@users[1]._id, @users[2]._id] - collaberator_refs: [@users[3]._id, @users[4]._id] - @db.users.find = (query, callback) => - callback null, [@user_lookup[query._id.toString()]] - sinon.spy @db.users, "find" - @ProjectGetter.populateProjectWithUsers @project, (err, project)=> - @callback err, project - - it "should look up each user", -> - for user in @users - @db.users.find.calledWith(_id: user._id).should.equal true - - it "should set the owner_ref to the owner", -> - @project.owner_ref.should.equal @users[0] - - it "should set the readOnly_refs to the read only users", -> - expect(@project.readOnly_refs).to.deep.equal [@users[1], @users[2]] - - it "should set the collaberator_refs to the collaborators", -> - expect(@project.collaberator_refs).to.deep.equal [@users[3], @users[4]] - - it "should call the callback", -> - assert.deepEqual @callback.args[0][1], @project - describe "findAllUsersProjects", -> beforeEach -> @fields = {"mock": "fields"} From a50bdaf5cc8985535b81145c5f97474d6f5a689e Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 15:32:04 +0000 Subject: [PATCH 06/48] Refactor LimitationsManager to use CollaboratorsHandler --- .../Subscription/LimitationsManager.coffee | 8 ++------ .../Subscription/LimitationsManagerTests.coffee | 15 ++------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index d323c19a9d..e2f633e185 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -3,6 +3,7 @@ Project = require("../../models/Project").Project User = require("../../models/User").User SubscriptionLocator = require("./SubscriptionLocator") Settings = require("settings-sharelatex") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = @@ -13,16 +14,11 @@ module.exports = callback null, owner.features.collaborators else callback null, Settings.defaultPlanCode.collaborators - - currentNumberOfCollaboratorsInProject: (project_id, callback) -> - Project.findById project_id, 'collaberator_refs readOnly_refs', (error, project) -> - return callback(error) if error? - callback null, (project.collaberator_refs.length + project.readOnly_refs.length) canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => return callback(error) if error? - @currentNumberOfCollaboratorsInProject project_id, (error, current_number) => + CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) => return callback(error) if error? if current_number + x_collaborators <= allowed_number or allowed_number < 0 callback null, true diff --git a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee index df1edca280..581d5cd6da 100644 --- a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee @@ -29,6 +29,7 @@ describe "LimitationsManager", -> '../../models/User' : User: @User './SubscriptionLocator':@SubscriptionLocator 'settings-sharelatex' : @Settings = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} 'logger-sharelatex':log:-> describe "allowedNumberOfCollaboratorsInProject", -> @@ -51,22 +52,10 @@ describe "LimitationsManager", -> it "should return the number of collaborators the user is allowed", -> @callback.calledWith(null, @user.features.collaborators).should.equal true - - describe "currentNumberOfCollaboratorsInProject", -> - beforeEach -> - @project.collaberator_refs = ["one", "two"] - @project.readOnly_refs = ["three"] - @callback = sinon.stub() - @LimitationsManager.currentNumberOfCollaboratorsInProject(@project_id, @callback) - - it "should return the total number of collaborators", -> - @callback.calledWith(null, 3).should.equal true describe "canAddXCollaborators", -> beforeEach -> - sinon.stub @LimitationsManager, - "currentNumberOfCollaboratorsInProject", - (project_id, callback) => callback(null, @current_number) + @CollaboratorsHandler.getCollaboratorCount = (project_id, callback) => callback(null, @current_number) sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => callback(null, @allowed_number) From 5f5445f625f3c2825f4200e3c3f648139d9dc6d2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 11:54:45 +0000 Subject: [PATCH 07/48] Use TpdsUpdateSender to use CollaboratorsHandler --- .../Features/Collaborators/CollaboratorsHandler.coffee | 2 +- .../web/app/coffee/Features/Project/ProjectGetter.coffee | 2 +- .../Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 9 ++++++--- .../Collaborators/CollaboratorsHandlerTests.coffee | 4 ++-- .../ThirdPartyDataStore/TpdsUpdateSenderTests.coffee | 6 +++++- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index ef2f96ea3d..72372345f8 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -54,7 +54,7 @@ module.exports = CollaboratorsHandler = return callback null, true, member.privilegeLevel return callback null, false, null - getProjectsUserIsMemberOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> + getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> callback(err, readAndWriteProjects, readOnlyProjects) diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 7d9fd13bb6..28f687116f 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -33,6 +33,6 @@ module.exports = ProjectGetter = CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" Project.find {owner_ref: user_id}, fields, (error, projects) -> return callback(error) if error? - CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> + CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> return callback(error) if error? callback null, projects, readAndWriteProjects, readOnlyProjects diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index f2d5bb94d1..88397c9ef6 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -5,6 +5,7 @@ Project = require('../../models/Project').Project keys = require('../../infrastructure/Keys') metrics = require("../../infrastructure/Metrics") request = require("request") +CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') buildPath = (user_id, project_name, filePath)-> projectPath = path.join(project_name, "/", filePath) @@ -116,9 +117,11 @@ module.exports = TpdsUpdateSender = TpdsUpdateSender._enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> - Project.findById project_id, "_id owner_ref readOnly_refs collaberator_refs", (err, project)-> - allUserIds = [].concat(project.collaberator_refs).concat(project.readOnly_refs).concat(project.owner_ref) - callback err, project.owner_ref, allUserIds + Project.findById project_id, "_id owner_ref", (err, project) -> + return callback(err) if err? + CollaboratorsHandler.getMemberIds project_id, (err, member_ids) -> + return callback(err) if err? + callback err, project.owner_ref, member_ids mergeProjectNameAndPath = (project_name, path)-> if(path.indexOf('/') == 0) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f0fd8db4a9..391b533cdf 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -112,13 +112,13 @@ describe "CollaboratorsHandler", -> .calledWith(null, false, null) .should.equal true - describe "getProjectsUserIsMemberOf", -> + describe "getProjectsUserIsCollaboratorOf", -> beforeEach -> @fields = "mock fields" @Project.find = sinon.stub() @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) - @CollaboratorHandler.getProjectsUserIsMemberOf @user_id, @fields, @callback + @CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback it "should call the callback with the projects", -> @callback diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index cd84080d17..bc2290eb04 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee @@ -20,7 +20,10 @@ filestoreUrl = "filestore.sharelatex.com" describe 'TpdsUpdateSender', -> beforeEach -> @requestQueuer = (queue, meth, opts, callback)-> - project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]} + project = {owner_ref:user_id} + member_ids = [collaberator_ref_1, read_only_ref_1, user_id] + @CollaboratorsHandler = + getMemberIds: sinon.stub().yields(null, member_ids) @Project = findById:sinon.stub().callsArgWith(2, null, project) @docstoreUrl = "docstore.sharelatex.env" @request = sinon.stub().returns(pipe:->) @@ -38,6 +41,7 @@ describe 'TpdsUpdateSender', -> "logger-sharelatex":{log:->} '../../models/Project': Project:@Project 'request':@request + '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler describe "_enqueue", -> From 40048d49a2f3b8627ad6f6c58aedb43582266383 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 12:07:42 +0000 Subject: [PATCH 08/48] Fix unit test --- .../test/UnitTests/coffee/Project/ProjectGetterTests.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index da9b9765e3..a0b2f19172 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -84,8 +84,8 @@ describe "ProjectGetter", -> @fields = {"mock": "fields"} @Project.find = sinon.stub() @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-projects"]) - @CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() - @CollaboratorsHandler.getProjectsUserIsMemberOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) + @CollaboratorsHandler.getProjectsUserIsCollaboratorOf = sinon.stub() + @CollaboratorsHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback it "should call the callback with all the projects", -> From b64c8e3d78fdd5366c40d42580597f6643f2ebdc Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 12:07:50 +0000 Subject: [PATCH 09/48] Delete dead code in User model --- services/web/app/coffee/models/User.coffee | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 8fca181d0e..0cd7196b64 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -55,32 +55,6 @@ UserSchema = new Schema # has this set to true, despite never having had a free trial hadFreeTrial: {type: Boolean, default: false} -UserSchema.statics.getAllIds = (callback)-> - this.find {}, ["first_name"], callback - - -UserSchema.statics.findReadOnlyProjects = (user_id, callback)-> - @find({'projects.readOnly_refs':user_id}).populate('projects.readOnly_refs').run (err, users)-> - projects = [] - _.each users, (user)-> - _.each user.projects, (project)-> - _.each project.readOnly_refs, (subUser)-> - if(subUser._id == user_id) - projects.push(project) - callback(projects) - -UserSchema.statics.findCollaborationProjects = (user_id, callback)-> - @find({'projects.collaberator_refs':user_id}).populate('projects.collaberator_refs').run (err, users)-> - projects = [] - _.each users, (user)-> - _.each user.projects, (project)-> - _.each project.collaberator_refs, (subUser)-> - if(subUser._id == user_id) - projects.push(project) - callback(projects) - - - conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) User = conn.model('User', UserSchema) From 0882eb2a996e385c1ceea6d89054302ba3290452 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:05:56 +0000 Subject: [PATCH 10/48] Don't use deprecated Project.findPopulatedById in ReferencesManager --- .../Features/Project/ProjectController.coffee | 1 - .../References/ReferencesHandler.coffee | 10 +++-- .../References/ReferencesHandlerTests.coffee | 44 +++++++++++-------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index b30c2b8f91..d89a014e2b 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -229,7 +229,6 @@ module.exports = ProjectController = title: project.name priority_title: true bodyClasses: ["editor"] - project : project project_id : project._id user : { id : user.id diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 0b2ddb1e26..1113a2f8a6 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -2,6 +2,7 @@ logger = require("logger-sharelatex") request = require("request") settings = require("settings-sharelatex") Project = require("../../models/Project").Project +User = require("../../models/User").User DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') U = require('underscore') Async = require('async') @@ -31,11 +32,12 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - owner = project.owner_ref - callback(null, owner.features.references == true) + User.find { _id: project.owner_ref }, { features: true }, (err, owner) -> + return callback(err) if err? + callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.findPopulatedById projectId, (err, project) -> + Project.find { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -44,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.findPopulatedById projectId, (err, project) -> + Project.find { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 86cf9111dc..3eacae73cd 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -41,9 +41,12 @@ describe 'ReferencesHandler', -> } '../../models/Project': { Project: @Project = { - findPopulatedById: sinon.stub().callsArgWith(1, null, @fakeProject) + find: sinon.stub().callsArgWith(1, null, @fakeProject) } } + '../../models/User': { + User: @User = {} + } '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null) } @@ -70,10 +73,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.findPopulatedById', (done) -> + it 'should call Project.find', (done) -> @call (err, data) => - @Project.findPopulatedById.callCount.should.equal 1 - @Project.findPopulatedById.calledWith(@projectId).should.equal true + @Project.find.callCount.should.equal 1 + @Project.find.calledWith(_id: @projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -109,10 +112,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when Project.find produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @Project.find.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -129,7 +132,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -147,7 +150,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -167,7 +170,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -182,7 +185,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -234,10 +237,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when Project.find produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @Project.find.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -254,7 +257,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -272,7 +275,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -312,16 +315,19 @@ describe 'ReferencesHandler', -> beforeEach -> @fakeProject = - owner_ref: - features: - references: false + owner_ref: @owner_ref = "owner-ref-123" + @owner = + features: + references: false + @User.find = sinon.stub() + @User.find.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback describe 'with references feature on', -> beforeEach -> - @fakeProject.owner_ref.features.references = true + @owner.features.references = true it 'should return true', -> @call (err, isFullIndex) => @@ -331,7 +337,7 @@ describe 'ReferencesHandler', -> describe 'with references feature off', -> beforeEach -> - @fakeProject.owner_ref.features.references = false + @owner.features.references = false it 'should return false', -> @call (err, isFullIndex) => From 359689ffea096c3f69fd46ab2ba348edc4871fcc Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:19:38 +0000 Subject: [PATCH 11/48] find -> findOne --- .../References/ReferencesHandler.coffee | 4 +-- .../References/ReferencesHandlerTests.coffee | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 1113a2f8a6..25bdbd443c 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -37,7 +37,7 @@ module.exports = ReferencesHandler = callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.find { _id: projectId }, (err, project) -> + Project.findOne { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -46,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.find { _id: projectId }, (err, project) -> + Project.findOne { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 3eacae73cd..f1ae93f3bb 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -41,7 +41,7 @@ describe 'ReferencesHandler', -> } '../../models/Project': { Project: @Project = { - find: sinon.stub().callsArgWith(1, null, @fakeProject) + findOne: sinon.stub().callsArgWith(1, null, @fakeProject) } } '../../models/User': { @@ -73,10 +73,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.find', (done) -> + it 'should call Project.findOne', (done) -> @call (err, data) => - @Project.find.callCount.should.equal 1 - @Project.find.calledWith(_id: @projectId).should.equal true + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith(_id: @projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -112,10 +112,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.find produces an error', -> + describe 'when Project.findOne produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, new Error('woops')) + @Project.findOne.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -132,7 +132,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -150,7 +150,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -170,7 +170,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -185,7 +185,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -237,10 +237,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.find produces an error', -> + describe 'when Project.findOne produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, new Error('woops')) + @Project.findOne.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -257,7 +257,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -275,7 +275,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) From 76af5e5563e6f5a89adbe842e48ca7d88998fa62 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:20:00 +0000 Subject: [PATCH 12/48] Don't call deprecated findPopulatedById in loadEditor --- .../coffee/Features/Project/ProjectController.coffee | 2 +- .../coffee/Project/ProjectControllerTests.coffee | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index d89a014e2b..74513ec3e1 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -187,7 +187,7 @@ module.exports = ProjectController = async.parallel { project: (cb)-> - Project.findPopulatedById project_id, cb + Project.findOne { _id: project_id }, cb user: (cb)-> if user_id == 'openUser' cb null, defaultSettingsForAnonymousUser(user_id) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 186c2e9836..73ada7a3cd 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -36,7 +36,7 @@ describe "ProjectController", -> @NotificationsHandler = getUserNotifications: sinon.stub() @ProjectModel = - findPopulatedById: sinon.stub() + findOne: sinon.stub() @UserModel = findById: sinon.stub() @SecurityManager = @@ -295,7 +295,7 @@ describe "ProjectController", -> fontSize:"massive" theme:"sexy" email: "bob@bob.com" - @ProjectModel.findPopulatedById.callsArgWith 1, null, @project + @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @@ -310,12 +310,6 @@ describe "ProjectController", -> done() @ProjectController.loadEditor @req, @res - it "should add the project onto the opts", (done)-> - @res.render = (pageName, opts)=> - opts.project.should.equal @project - done() - @ProjectController.loadEditor @req, @res - it "should add user", (done)-> @res.render = (pageName, opts)=> opts.user.email.should.equal @user.email From e53fc5f0b652599138a2a7a4bc59700e43b6adf3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:20:53 +0000 Subject: [PATCH 13/48] Remove dead code (Project.findPopulatedById) --- services/web/app/coffee/models/Project.coffee | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 3a391adfc0..147ff12911 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -43,23 +43,6 @@ ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> return callback(new Errors.NotFoundError(e.message)) this.findById project_or_id, fields, callback -ProjectSchema.statics.findPopulatedById = (project_id, callback)-> - logger.log project_id:project_id, "findPopulatedById" - this.find(_id: project_id ) - .populate('collaberator_refs') - .populate('readOnly_refs') - .populate('owner_ref') - .exec (err, projects)-> - if err? - logger.err err:err, project_id:project_id, "something went wrong looking for project findPopulatedById" - callback(err) - else if !projects? || projects.length == 0 - logger.err project_id:project_id, "something went wrong looking for project findPopulatedById, no project could be found" - callback "not found" - else - logger.log project_id:project_id, "finished findPopulatedById" - callback(null, projects[0]) - sanitizeTypeOfElement = (elementType)-> lastChar = elementType.slice -1 if lastChar != "s" From 3e423b8a0690763d589e7b26cee403c29b09628c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 8 Mar 2016 14:38:25 +0000 Subject: [PATCH 14/48] Another find->findOne --- .../web/app/coffee/Features/References/ReferencesHandler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 25bdbd443c..b64600fdb1 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -32,7 +32,7 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - User.find { _id: project.owner_ref }, { features: true }, (err, owner) -> + User.findOne { _id: project.owner_ref }, { features: true }, (err, owner) -> return callback(err) if err? callback(null, owner?.features?.references == true) From 37c966ba7ebedbf9c32df2bc1017e0a18bcfb291 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:42:11 +0000 Subject: [PATCH 15/48] Fix unit test --- .../UnitTests/coffee/References/ReferencesHandlerTests.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index f1ae93f3bb..1666a2def1 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -319,8 +319,8 @@ describe 'ReferencesHandler', -> @owner = features: references: false - @User.find = sinon.stub() - @User.find.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) + @User.findOne = sinon.stub() + @User.findOne.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback From e1fa77dd72e0dc789dff951167a9470a53eb60f4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 15:59:04 +0000 Subject: [PATCH 16/48] Add beginnings of acceptance tests --- services/web/.gitignore | 1 + services/web/Gruntfile.coffee | 17 ++++ services/web/package.json | 3 +- .../coffee/AuthorizationTests.coffee | 86 +++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 services/web/test/acceptance/coffee/AuthorizationTests.coffee diff --git a/services/web/.gitignore b/services/web/.gitignore index f8a1fc0842..a3f982ef53 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -40,6 +40,7 @@ app.js app/js/* test/UnitTests/js/* test/smoke/js/* +test/acceptance/js/* cookies.txt requestQueueWorker.js TpdsWorker.js diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 7d49df07af..3cfe803b17 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -71,6 +71,14 @@ module.exports = (grunt) -> dest: 'test/UnitTests/js/', ext: '.js' + acceptance_tests: + expand: true, + flatten: false, + cwd: 'test/acceptance/coffee', + src: ['**/*.coffee'], + dest: 'test/acceptance/js/', + ext: '.js' + less: app: files: @@ -119,6 +127,7 @@ module.exports = (grunt) -> clean: app: ["app/js"] unit_tests: ["test/UnitTests/js"] + acceptance_tests: ["test/acceptance/js"] mochaTest: unit: @@ -131,6 +140,11 @@ module.exports = (grunt) -> options: reporter: grunt.option('reporter') or 'spec' grep: grunt.option("grep") + acceptance: + src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"] + options: + reporter: grunt.option('reporter') or 'spec' + grep: grunt.option("grep") "git-rev-parse": version: @@ -184,6 +198,7 @@ module.exports = (grunt) -> ] "Test tasks": [ "test:unit" + "test:acceptance" ] "Run tasks": [ "run" @@ -290,6 +305,7 @@ module.exports = (grunt) -> grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] + grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests'] grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests'] grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css'] @@ -297,6 +313,7 @@ module.exports = (grunt) -> grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile'] grunt.registerTask 'test:unit', 'Run the unit tests (use --grep= or --feature= for individual tests)', ['compile:server', 'compile:modules:server', 'compile:unit_tests', 'compile:modules:unit_tests', 'mochaTest:unit'].concat(moduleUnitTestTasks) + grunt.registerTask 'test:acceptance', 'Run the acceptance tests (use --grep= or --feature= for individual tests)', ['compile:acceptance_tests', 'mochaTest:acceptance'] grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke'] grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks) diff --git a/services/web/package.json b/services/web/package.json index 637df85a8f..021d86f6d7 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -44,7 +44,8 @@ "redback": "0.4.0", "redis": "0.10.1", "redis-sharelatex": "0.0.9", - "request": "2.34.0", + "request": "^2.69.0", + "requests": "^0.1.7", "rimraf": "2.2.6", "sanitizer": "0.1.1", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee new file mode 100644 index 0000000000..d5c1602ca6 --- /dev/null +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -0,0 +1,86 @@ +request = require("request") +expect = require("chai").expect + +count = 0 +BASE_URL = "http://localhost:3000" + +request = request.defaults({ + baseUrl: BASE_URL, + followRedirect: false +}) + +class User + constructor: (options = {}) -> + @email = "acceptance-test-#{count}@example.com" + @password = "acceptance-test-#{count}-password" + count++ + @jar = request.jar() + @request = request.defaults({ + jar: @jar + }) + + login: (callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/register" # Register will log in, but also ensure user exists + json: + email: @email + password: @password + }, (error, response, body) => + return callback(error) if error? + callback() + + createProject: (name, callback = (error, project_id) ->) -> + @request.post { + url: "/project/new", + json: + projectName: name + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.project_id) + + getCsrfToken: (callback = (error) ->) -> + @request.get { + url: "/register" + }, (err, response, body) => + return callback(error) if error? + csrfMatches = body.match("window.csrfToken = \"(.*?)\";") + if !csrfMatches? + return callback(new Error("no csrf token found")) + @request = @request.defaults({ + headers: + "x-csrf-token": csrfMatches[1] + }) + callback() + +describe "Authorization", -> + describe "private project", -> + before (done) -> + @owner = new User() + @other = new User() + @owner.login (error) => + return done(error) if error? + @other.login (error) => + return done(error) if error? + @owner.createProject "private-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + done() + + it "should allow the owner to access it", (done) -> + @owner.request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 200 + done() + + it "should not allow another user to access it", (done) -> + @other.request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + done() + + it "should not allow anonymous user to access it", (done) -> + request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + done() \ No newline at end of file From 4f9f255153ef12d7a538760417329b367e44d7c4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 12:31:46 +0000 Subject: [PATCH 17/48] Extend acceptance tests to include shared projects --- .../Collaborators/CollaboratorsHandler.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 196 ++++++++++++++++-- 2 files changed, 174 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 72372345f8..f6b8ca7bbd 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -1,6 +1,5 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project -ProjectEntityHandler = require("../Project/ProjectEntityHandler") mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" @@ -108,6 +107,7 @@ module.exports = CollaboratorsHandler = Project.update { _id: project_id }, { $addToSet: level }, (error) -> return callback(error) if error? # Flush to TPDS in background to add files to collaborator's Dropbox + ProjectEntityHandler = require("../Project/ProjectEntityHandler") ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) -> if error? logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index d5c1602ca6..478ef598a0 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,5 +1,6 @@ request = require("request") expect = require("chai").expect +async = require "async" count = 0 BASE_URL = "http://localhost:3000" @@ -40,6 +41,14 @@ class User return callback(error) if error? callback(null, body.project_id) + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> + @request.post { + url: "/project/#{project_id}/users", + json: {email, privileges} + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.user) + getCsrfToken: (callback = (error) ->) -> @request.get { url: "/register" @@ -54,33 +63,174 @@ class User }) callback() +try_read_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.get "/project/#{project_id}", (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + (cb) -> + requester.get "/project/#{project_id}/download/zip", (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +try_write_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.post { + uri: "/project/#{project_id}/settings" + json: + compiler: "latex" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +try_admin_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.post { + uri: "/project/#{project_id}/rename" + json: + newProjectName: "new-name" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +expect_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_no_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_anonymous_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + +expect_no_anonymous_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + +expect_no_anonymous_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + describe "Authorization", -> + before (done) -> + @timeout(10000) + @owner = new User() + @other1 = new User() + @other2 = new User() + @anon = new User() + async.parallel [ + (cb) => @owner.login cb + (cb) => @other1.login cb + (cb) => @other2.login cb + (cb) => @anon.getCsrfToken cb + ], done + describe "private project", -> before (done) -> - @owner = new User() - @other = new User() - @owner.login (error) => + @owner.createProject "private-project", (error, project_id) => return done(error) if error? - @other.login (error) => - return done(error) if error? - @owner.createProject "private-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - done() + @project_id = project_id + done() - it "should allow the owner to access it", (done) -> - @owner.request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 200 - done() + it "should allow the owner read access to it", (done) -> + expect_read_access @owner.request, @project_id, done - it "should not allow another user to access it", (done) -> - @other.request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" - done() + it "should allow the owner write access to it", (done) -> + expect_write_access @owner.request, @project_id, done + + it "should allow the owner admin access to it", (done) -> + expect_admin_access @owner.request, @project_id, done - it "should not allow anonymous user to access it", (done) -> - request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - done() \ No newline at end of file + it "should not allow another user read access to it", (done) -> + expect_no_read_access(@other1.request, @project_id, done) + + it "should not allow another user write access to it", (done) -> + expect_no_write_access(@other1.request, @project_id, done) + + it "should not allow another user admin access to it", (done) -> + expect_no_admin_access(@other1.request, @project_id, done) + + it "should not allow anonymous user read access to it", (done) -> + expect_no_anonymous_read_access(@anon.request, @project_id, done) + + it "should not allow anonymous user write access to it", (done) -> + expect_no_anonymous_write_access(@anon.request, @project_id, done) + + it "should not allow anonymous user write access to it", (done) -> + expect_no_anonymous_admin_access(@anon.request, @project_id, done) + + describe "shared project", -> + before (done) -> + @rw_user = @other1 + @ro_user = @other2 + @owner.createProject "private-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.addUserToProject @project_id, @ro_user.email, "readOnly", (error) => + return done(error) if error? + @owner.addUserToProject @project_id, @rw_user.email, "readAndWrite", (error) => + return done(error) if error? + done() + + it "should allow the read-only user read access to it", (done) -> + expect_read_access @ro_user.request, @project_id, done + + it "should not allow the read-only user write access to it", (done) -> + expect_no_write_access @ro_user.request, @project_id, done + + it "should not allow the read-only user admin access to it", (done) -> + expect_no_admin_access @ro_user.request, @project_id, done + + it "should allow the read-write user read access to it", (done) -> + expect_read_access @rw_user.request, @project_id, done + + it "should allow the read-write user write access to it", (done) -> + expect_write_access @rw_user.request, @project_id, done + + it "should not allow the read-write user admin access to it", (done) -> + expect_no_admin_access @rw_user.request, @project_id, done + + \ No newline at end of file From 2116d0271c047166a13ad96b00f5b43cf5eb22af Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 15:30:23 +0000 Subject: [PATCH 18/48] Update acceptance tests for public projects --- services/web/config/settings.defaults.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 107 ++++++++++-------- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index d900d17062..b45837193b 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -256,7 +256,7 @@ module.exports = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. - allowPublicAccess: false + allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"]? then true else false # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 478ef598a0..6c7ff65539 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -49,6 +49,15 @@ class User return callback(error) if error? callback(null, body.user) + makePublic: (project_id, level, callback = (error) ->) -> + @request.post { + url: "/project/#{project_id}/settings", + json: + publicAccessLevel: level + }, (error, response, body) -> + return callback(error) if error? + callback(null) + getCsrfToken: (callback = (error) ->) -> @request.get { url: "/register" @@ -77,7 +86,7 @@ try_read_access = (requester, project_id, test, callback) -> cb() ], callback -try_write_access = (requester, project_id, test, callback) -> +try_settings_write_access = (requester, project_id, test, callback) -> async.parallel [ (cb) -> requester.post { @@ -105,11 +114,13 @@ try_admin_access = (requester, project_id, test, callback) -> expect_read_access = (requester, project_id, callback) -> try_read_access(requester, project_id, (response, body) -> + if response.statusCode not in [200,204] + console.log response.statusCode, response.headers expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> +expect_settings_write_access = (requester, project_id, callback) -> + try_settings_write_access(requester, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) @@ -118,40 +129,22 @@ expect_admin_access = (requester, project_id, callback) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_no_read_access = (requester, project_id, callback) -> +expect_no_read_access = (requester, project_id, options, callback) -> try_read_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" + expect(response.headers.location).to.equal options.redirect_to , callback) -expect_no_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> +expect_no_settings_write_access = (requester, project_id, options, callback) -> + try_settings_write_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" + expect(response.headers.location).to.equal options.redirect_to , callback) -expect_no_admin_access = (requester, project_id, callback) -> +expect_no_admin_access = (requester, project_id, options, callback) -> try_admin_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" - , callback) - -expect_no_anonymous_read_access = (requester, project_id, callback) -> - try_read_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - , callback) - -expect_no_anonymous_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - , callback) - -expect_no_anonymous_admin_access = (requester, project_id, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" + expect(response.headers.location).to.equal options.redirect_to , callback) describe "Authorization", -> @@ -178,29 +171,29 @@ describe "Authorization", -> it "should allow the owner read access to it", (done) -> expect_read_access @owner.request, @project_id, done - it "should allow the owner write access to it", (done) -> - expect_write_access @owner.request, @project_id, done + it "should allow the owner write access to its settings", (done) -> + expect_settings_write_access @owner.request, @project_id, done it "should allow the owner admin access to it", (done) -> expect_admin_access @owner.request, @project_id, done it "should not allow another user read access to it", (done) -> - expect_no_read_access(@other1.request, @project_id, done) + expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow another user write access to it", (done) -> - expect_no_write_access(@other1.request, @project_id, done) + it "should not allow another user write access to its settings", (done) -> + expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done it "should not allow another user admin access to it", (done) -> - expect_no_admin_access(@other1.request, @project_id, done) + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_anonymous_read_access(@anon.request, @project_id, done) + expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow anonymous user write access to it", (done) -> - expect_no_anonymous_write_access(@anon.request, @project_id, done) + it "should not allow anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow anonymous user write access to it", (done) -> - expect_no_anonymous_admin_access(@anon.request, @project_id, done) + it "should not allow anonymous user admin access to it", (done) -> + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done describe "shared project", -> before (done) -> @@ -218,19 +211,43 @@ describe "Authorization", -> it "should allow the read-only user read access to it", (done) -> expect_read_access @ro_user.request, @project_id, done - it "should not allow the read-only user write access to it", (done) -> - expect_no_write_access @ro_user.request, @project_id, done + it "should not allow the read-only user write access to its settings", (done) -> + expect_no_settings_write_access @ro_user.request, @project_id, redirect_to: "/restricted", done it "should not allow the read-only user admin access to it", (done) -> - expect_no_admin_access @ro_user.request, @project_id, done + expect_no_admin_access @ro_user.request, @project_id, redirect_to: "/restricted", done it "should allow the read-write user read access to it", (done) -> expect_read_access @rw_user.request, @project_id, done - it "should allow the read-write user write access to it", (done) -> - expect_write_access @rw_user.request, @project_id, done + it "should allow the read-write user write access to its settings", (done) -> + expect_settings_write_access @rw_user.request, @project_id, done it "should not allow the read-write user admin access to it", (done) -> - expect_no_admin_access @rw_user.request, @project_id, done + expect_no_admin_access @rw_user.request, @project_id, redirect_to: "/restricted", done + describe "public read-write project", -> + before (done) -> + @owner.createProject "public-rw-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.makePublic @project_id, "readAndWrite", done + + it "should allow a user read access to it", (done) -> + expect_read_access @other1.request, @project_id, done + + it "should not allow a user write access to its settings"#, (done) -> + # expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + + it "should not allow a user admin access to it", (done) -> + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + + it "should allow anonymous user read access to it", (done) -> + expect_read_access @anon.request, @project_id, done + + it "should not allow anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon.request, @project_id, redirect_to: "/restricted", done + + it "should not allow anonymous user admin access to it", (done) -> + expect_no_admin_access @anon.request, @project_id, redirect_to: "/restricted", done \ No newline at end of file From c46c083b3191bdc0caaa9e0ca4f23b45e5d30928 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 16:26:18 +0000 Subject: [PATCH 19/48] Check write access to documents via real-time end point --- .../coffee/AuthorizationTests.coffee | 164 ++++++++++++------ 1 file changed, 110 insertions(+), 54 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 6c7ff65539..a5eaa12fda 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,6 +1,8 @@ request = require("request") expect = require("chai").expect async = require "async" +settings = require("settings-sharelatex") +{db} = require("../../../app/js/infrastructure/mongojs") count = 0 BASE_URL = "http://localhost:3000" @@ -30,7 +32,10 @@ class User password: @password }, (error, response, body) => return callback(error) if error? - callback() + db.users.findOne {email: @email}, (error, user) => + return callback(error) if error? + @id = user?._id?.toString() + callback() createProject: (name, callback = (error, project_id) ->) -> @request.post { @@ -72,24 +77,24 @@ class User }) callback() -try_read_access = (requester, project_id, test, callback) -> +try_read_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.get "/project/#{project_id}", (error, response, body) -> + user.request.get "/project/#{project_id}", (error, response, body) -> return cb(error) if error? test(response, body) cb() (cb) -> - requester.get "/project/#{project_id}/download/zip", (error, response, body) -> + user.request.get "/project/#{project_id}/download/zip", (error, response, body) -> return cb(error) if error? test(response, body) cb() ], callback -try_settings_write_access = (requester, project_id, test, callback) -> +try_settings_write_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.post { + user.request.post { uri: "/project/#{project_id}/settings" json: compiler: "latex" @@ -99,10 +104,10 @@ try_settings_write_access = (requester, project_id, test, callback) -> cb() ], callback -try_admin_access = (requester, project_id, test, callback) -> +try_admin_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.post { + user.request.post { uri: "/project/#{project_id}/rename" json: newProjectName: "new-name" @@ -112,39 +117,78 @@ try_admin_access = (requester, project_id, test, callback) -> cb() ], callback -expect_read_access = (requester, project_id, callback) -> - try_read_access(requester, project_id, (response, body) -> - if response.statusCode not in [200,204] - console.log response.statusCode, response.headers +try_content_access = (user, project_id, test, callback) -> + # The real-time service calls this end point to determine the user's + # permissions. + request.post { + url: "/project/#{project_id}/join" + qs: {user_id: user.id} + auth: + user: settings.apis.web.user + pass: settings.apis.web.pass + sendImmediately: true + json: true + jar: false + }, (error, response, body) -> + return callback(error) if error? + test(response, body) + callback() + +expect_read_access = (user, project_id, callback) -> + async.series [ + (cb) -> + try_read_access(user, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , cb) + (cb) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite", "readOnly"] + , cb) + ], callback + +expect_content_write_access = (user, project_id, callback) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite"] + , callback) + +expect_settings_write_access = (user, project_id, callback) -> + try_settings_write_access(user, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_settings_write_access = (requester, project_id, callback) -> - try_settings_write_access(requester, project_id, (response, body) -> +expect_admin_access = (user, project_id, callback) -> + try_admin_access(user, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_admin_access = (requester, project_id, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.be.oneOf [200, 204] +expect_no_read_access = (user, project_id, options, callback) -> + async.series [ + (cb) -> + try_read_access(user, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.match new RegExp(options.redirect_to) + , cb) + (cb) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.equal false + , cb) + ], callback + +expect_no_content_write_access = (user, project_id, callback) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf [false, "readOnly"] , callback) -expect_no_read_access = (requester, project_id, options, callback) -> - try_read_access(requester, project_id, (response, body) -> +expect_no_settings_write_access = (user, project_id, options, callback) -> + try_settings_write_access(user, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to + expect(response.headers.location).to.match new RegExp(options.redirect_to) , callback) -expect_no_settings_write_access = (requester, project_id, options, callback) -> - try_settings_write_access(requester, project_id, (response, body) -> +expect_no_admin_access = (user, project_id, options, callback) -> + try_admin_access(user, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to - , callback) - -expect_no_admin_access = (requester, project_id, options, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to + expect(response.headers.location).to.match new RegExp(options.redirect_to) , callback) describe "Authorization", -> @@ -169,31 +213,31 @@ describe "Authorization", -> done() it "should allow the owner read access to it", (done) -> - expect_read_access @owner.request, @project_id, done + expect_read_access @owner, @project_id, done it "should allow the owner write access to its settings", (done) -> - expect_settings_write_access @owner.request, @project_id, done + expect_settings_write_access @owner, @project_id, done it "should allow the owner admin access to it", (done) -> - expect_admin_access @owner.request, @project_id, done + expect_admin_access @owner, @project_id, done - it "should not allow another user read access to it", (done) -> - expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done + it "should not allow another user read access to the project", (done) -> + expect_no_read_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow another user write access to its settings", (done) -> - expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow another user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_read_access @anon, @project_id, redirect_to: "/login", done it "should not allow anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done describe "shared project", -> before (done) -> @@ -209,22 +253,28 @@ describe "Authorization", -> done() it "should allow the read-only user read access to it", (done) -> - expect_read_access @ro_user.request, @project_id, done + expect_read_access @ro_user, @project_id, done + + it "should not allow the read-only user write access to its content", (done) -> + expect_no_content_write_access @ro_user, @project_id, done it "should not allow the read-only user write access to its settings", (done) -> - expect_no_settings_write_access @ro_user.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @ro_user, @project_id, redirect_to: "/restricted", done it "should not allow the read-only user admin access to it", (done) -> - expect_no_admin_access @ro_user.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @ro_user, @project_id, redirect_to: "/restricted", done it "should allow the read-write user read access to it", (done) -> - expect_read_access @rw_user.request, @project_id, done + expect_read_access @rw_user, @project_id, done + + it "should allow the read-write user write access to its content", (done) -> + expect_content_write_access @rw_user, @project_id, done it "should allow the read-write user write access to its settings", (done) -> - expect_settings_write_access @rw_user.request, @project_id, done + expect_settings_write_access @rw_user, @project_id, done it "should not allow the read-write user admin access to it", (done) -> - expect_no_admin_access @rw_user.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @rw_user, @project_id, redirect_to: "/restricted", done describe "public read-write project", -> before (done) -> @@ -234,20 +284,26 @@ describe "Authorization", -> @owner.makePublic @project_id, "readAndWrite", done it "should allow a user read access to it", (done) -> - expect_read_access @other1.request, @project_id, done + expect_read_access @other1, @project_id, done + + it "should allow a user write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done - it "should allow anonymous user read access to it", (done) -> - expect_read_access @anon.request, @project_id, done + it "should allow an anonymous user read access to it", (done) -> + expect_read_access @anon, @project_id, done - it "should not allow anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @anon.request, @project_id, redirect_to: "/restricted", done + it "should allow an anonymous user write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done - it "should not allow anonymous user admin access to it", (done) -> - expect_no_admin_access @anon.request, @project_id, redirect_to: "/restricted", done + it "should not allow an anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done + + it "should not allow an anonymous user admin access to it", (done) -> + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done \ No newline at end of file From d235ab22ed6a7121ea097abc371b1a71a5a88cff Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 16:28:46 +0000 Subject: [PATCH 20/48] Add in tests for public read-only projects --- .../coffee/AuthorizationTests.coffee | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index a5eaa12fda..ad6f817429 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -287,7 +287,7 @@ describe "Authorization", -> expect_read_access @other1, @project_id, done it "should allow a user write access to its content", (done) -> - expect_content_write_access @owner, @project_id, done + expect_content_write_access @other1, @project_id, done it "should not allow a user write access to its settings"#, (done) -> # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done @@ -299,11 +299,41 @@ describe "Authorization", -> expect_read_access @anon, @project_id, done it "should allow an anonymous user write access to its content", (done) -> - expect_content_write_access @owner, @project_id, done + expect_content_write_access @anon, @project_id, done it "should not allow an anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow an anonymous user admin access to it", (done) -> expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done - \ No newline at end of file + + describe "public read-only project", -> + before (done) -> + @owner.createProject "public-ro-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.makePublic @project_id, "readOnly", done + + it "should allow a user read access to it", (done) -> + expect_read_access @other1, @project_id, done + + it "should not allow a user write access to its content", (done) -> + expect_no_content_write_access @other1, @project_id, done + + it "should not allow a user write access to its settings"#, (done) -> + # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + + it "should not allow a user admin access to it", (done) -> + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done + + it "should allow an anonymous user read access to it", (done) -> + expect_read_access @anon, @project_id, done + + it "should not allow an anonymous user write access to its content", (done) -> + expect_no_content_write_access @anon, @project_id, done + + it "should not allow an anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done + + it "should not allow an anonymous user admin access to it", (done) -> + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done \ No newline at end of file From e36be96ec93fc06e5d91d0f566cd3f2cf0253e93 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 11:13:57 +0000 Subject: [PATCH 21/48] Move public access setting to its own end point --- services/web/Gruntfile.coffee | 1 + .../Features/Project/ProjectController.coffee | 8 ++++++ services/web/app/coffee/router.coffee | 1 + .../ide/settings/services/settings.coffee | 5 ++++ .../ShareProjectModalController.coffee | 4 +-- .../Project/ProjectControllerTests.coffee | 25 ++++++++++--------- .../coffee/AuthorizationTests.coffee | 19 ++++++++++---- 7 files changed, 44 insertions(+), 19 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 3cfe803b17..428e4d506d 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -143,6 +143,7 @@ module.exports = (grunt) -> acceptance: src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"] options: + timeout: 10000 reporter: grunt.option('reporter') or 'spec' grep: grunt.option("grep") diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 74513ec3e1..0ca14090c2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -42,6 +42,14 @@ module.exports = ProjectController = jobs.push (callback) -> editorController.setRootDoc project_id, req.body.rootDocId, callback + async.series jobs, (error) -> + return next(error) if error? + res.sendStatus(204) + + updateProjectAdminSettings: (req, res, next) -> + project_id = req.params.Project_id + + jobs = [] if req.body.publicAccessLevel? jobs.push (callback) -> editorController.setPublicAccessLevel project_id, req.body.publicAccessLevel, callback diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 46b1db2157..eafe41470b 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -103,6 +103,7 @@ module.exports = class Router }), SecurityManager.requestCanAccessProject, ProjectController.loadEditor webRouter.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings + webRouter.post '/project/:Project_id/settings/admin', SecurityManager.requestIsOwner, ProjectController.updateProjectAdminSettings webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile webRouter.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 78dda105ad..4e6bbcea3d 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -10,5 +10,10 @@ define [ saveProjectSettings: (data) -> data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings", data + + saveProjectAdminSettings: (data) -> + data._csrf = window.csrfToken + ide.$http.post "/project/#{ide.project_id}/settings/admin", data + } ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index bb945a9ebc..13d5faea9f 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -143,7 +143,7 @@ define [ $scope.makePublic = () -> $scope.project.publicAccesLevel = $scope.inputs.privileges - settings.saveProjectSettings({publicAccessLevel: $scope.inputs.privileges}) + settings.saveProjectAdminSettings({publicAccessLevel: $scope.inputs.privileges}) $modalInstance.close() $scope.cancel = () -> @@ -153,7 +153,7 @@ define [ App.controller "MakePrivateModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) -> $scope.makePrivate = () -> $scope.project.publicAccesLevel = "private" - settings.saveProjectSettings({publicAccessLevel: "private"}) + settings.saveProjectAdminSettings({publicAccessLevel: "private"}) $modalInstance.close() $scope.cancel = () -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 73ada7a3cd..0be616da51 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -126,18 +126,6 @@ describe "ProjectController", -> done() @ProjectController.updateProjectSettings @req, @res - it "should update the public access level", (done) -> - @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) - @req.body = - publicAccessLevel: @publicAccessLevel = "readonly" - @res.sendStatus = (code) => - @EditorController.setPublicAccessLevel - .calledWith(@project_id, @publicAccessLevel) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - it "should update the root doc", (done) -> @EditorController.setRootDoc = sinon.stub().callsArg(2) @req.body = @@ -149,6 +137,19 @@ describe "ProjectController", -> code.should.equal 204 done() @ProjectController.updateProjectSettings @req, @res + + describe "updateProjectAdminSettings", -> + it "should update the public access level", (done) -> + @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) + @req.body = + publicAccessLevel: @publicAccessLevel = "readonly" + @res.sendStatus = (code) => + @EditorController.setPublicAccessLevel + .calledWith(@project_id, @publicAccessLevel) + .should.equal true + code.should.equal 204 + done() + @ProjectController.updateProjectAdminSettings @req, @res describe "deleteProject", -> it "should tell the project deleter to archive when forever=false", (done)-> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index ad6f817429..a9533e9533 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -56,7 +56,7 @@ class User makePublic: (project_id, level, callback = (error) ->) -> @request.post { - url: "/project/#{project_id}/settings", + url: "/project/#{project_id}/settings/admin", json: publicAccessLevel: level }, (error, response, body) -> @@ -78,7 +78,7 @@ class User callback() try_read_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.get "/project/#{project_id}", (error, response, body) -> return cb(error) if error? @@ -92,7 +92,7 @@ try_read_access = (user, project_id, test, callback) -> ], callback try_settings_write_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.post { uri: "/project/#{project_id}/settings" @@ -105,7 +105,7 @@ try_settings_write_access = (user, project_id, test, callback) -> ], callback try_admin_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.post { uri: "/project/#{project_id}/rename" @@ -115,6 +115,15 @@ try_admin_access = (user, project_id, test, callback) -> return cb(error) if error? test(response, body) cb() + (cb) -> + user.request.post { + uri: "/project/#{project_id}/settings/admin" + json: + publicAccessLevel: "private" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() ], callback try_content_access = (user, project_id, test, callback) -> @@ -198,7 +207,7 @@ describe "Authorization", -> @other1 = new User() @other2 = new User() @anon = new User() - async.parallel [ + async.series [ (cb) => @owner.login cb (cb) => @other1.login cb (cb) => @other2.login cb From 3e03164ed4e6f29f669d74fe0cab7f19e2359dca Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 17:15:14 +0000 Subject: [PATCH 22/48] Remove dead auth_token code --- .../AuthenticationController.coffee | 35 +----- .../AuthenticationManager.coffee | 16 --- .../CollaboratorsController.coffee | 32 ----- .../Collaborators/CollaboratorsRouter.coffee | 1 - .../Features/User/UserInfoController.coffee | 4 - services/web/app/coffee/router.coffee | 3 +- .../AuthenticationControllerTests.coffee | 114 ++++-------------- .../AuthenticationManagerTests.coffee | 46 ------- .../CollaboratorsControllerTests.coffee | 46 ------- 9 files changed, 30 insertions(+), 267 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 312b441158..e7db5d9f65 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -44,50 +44,27 @@ module.exports = AuthenticationController = text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error' - getAuthToken: (req, res, next = (error) ->) -> - AuthenticationController.getLoggedInUserId req, (error, user_id) -> - return next(error) if error? - AuthenticationManager.getAuthToken user_id, (error, auth_token) -> - return next(error) if error? - res.send(auth_token) - getLoggedInUserId: (req, callback = (error, user_id) ->) -> if req?.session?.user?._id? callback null, req.session.user._id.toString() else callback null, null - getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) -> - if typeof(options) == "function" - callback = options - options = {allow_auth_token: false} - + getLoggedInUser: (req, callback = (error, user) ->) -> if req.session?.user?._id? query = req.session.user._id - else if req.query?.auth_token? and options.allow_auth_token - query = { auth_token: req.query.auth_token } else return callback null, null UserGetter.getUser query, callback - requireLogin: (options = {allow_auth_token: false, load_from_db: false}) -> + requireLogin: () -> doRequest = (req, res, next = (error) ->) -> - load_from_db = options.load_from_db - if req.query?.auth_token? and options.allow_auth_token - load_from_db = true - if load_from_db - AuthenticationController.getLoggedInUser req, { allow_auth_token: options.allow_auth_token }, (error, user) -> - return next(error) if error? - return AuthenticationController._redirectToLoginOrRegisterPage(req, res) if !user? - req.user = user - return next() + if !req.session.user? + AuthenticationController._redirectToLoginOrRegisterPage(req, res) else - if !req.session.user? - AuthenticationController._redirectToLoginOrRegisterPage(req, res) - else - req.user = req.session.user - return next() + req.user = req.session.user + return next() return doRequest diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index 254c5c6c41..d2422be90b 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -36,19 +36,3 @@ module.exports = AuthenticationManager = $unset: password: true }, callback) - getAuthToken: (user_id, callback = (error, auth_token) ->) -> - db.users.findOne { _id: ObjectId(user_id.toString()) }, { auth_token : true }, (error, user) => - return callback(error) if error? - return callback(new Error("user could not be found: #{user_id}")) if !user? - if user.auth_token? - callback null, user.auth_token - else - @_createSecureToken (error, auth_token) -> - db.users.update { _id: ObjectId(user_id.toString()) }, { $set : auth_token: auth_token }, (error) -> - return callback(error) if error? - callback null, auth_token - - _createSecureToken: (callback = (error, token) ->) -> - crypto.randomBytes 48, (error, buffer) -> - return callback(error) if error? - callback null, buffer.toString("hex") diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 82912e1b6b..1905d0d0a6 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -7,14 +7,6 @@ UserGetter = require "../User/UserGetter" mimelib = require("mimelib") module.exports = CollaboratorsController = - getCollaborators: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - CollaboratorsHandler.getMembersWithPrivilegeLevels project_id, (error, members) -> - return next(error) if error? - CollaboratorsController._formatCollaborators members, (error, collaborators) -> - return next(error) if error? - res.json(collaborators) - addUserToProject: (req, res, next) -> project_id = req.params.Project_id LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => @@ -58,27 +50,3 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - _formatCollaborators: (members, callback = (error, collaborators) ->) -> - collaborators = [] - - pushCollaborator = (user, permissions, owner) -> - collaborators.push { - id: user._id.toString() - first_name: user.first_name - last_name: user.last_name - email: user.email - permissions: permissions - owner: owner - } - - for member in members - {user, privilegeLevel} = member - if privilegeLevel == "admin" - pushCollaborator(user, ["read", "write", "admin"], true) - else if privilegeLevel == "readAndWrite" - pushCollaborator(user, ["read", "write"], false) - else - pushCollaborator(user, ["read"], false) - - callback null, collaborators - diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index d5abf23350..f4f1b0343c 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -5,7 +5,6 @@ AuthenticationController = require('../Authentication/AuthenticationController') module.exports = apply: (webRouter, apiRouter) -> webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - apiRouter.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index ac7556bc90..edaf836c80 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -6,10 +6,6 @@ sanitize = require('sanitizer') module.exports = UserController = getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) -> - # this is funcky as hell, we don't use the current session to get the user - # we use the auth token, actually destroying session from the chat api request - if req.query?.auth_token? - req.session?.destroy() logger.log user: req.user, "reciving request for getting logged in users personal info" return next(new Error("User is not logged in")) if !req.user? UserGetter.getUser req.user._id, { diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index eafe41470b..36b1bd7ea2 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -88,8 +88,7 @@ module.exports = class Router webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser - webRouter.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken - webRouter.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo + webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 63ff12a374..f83b38617b 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -173,7 +173,7 @@ describe "AuthenticationController", -> beforeEach -> @req.session = user: @user - @AuthenticationController.getLoggedInUser(@req, {}, @callback) + @AuthenticationController.getLoggedInUser(@req, @callback) it "should look up the user in the database", -> @UserGetter.getUser @@ -183,105 +183,37 @@ describe "AuthenticationController", -> it "should return the user", -> @callback.calledWith(null, @user).should.equal true - describe "with an auth token, but without auth_token_allowed set to true", -> - beforeEach -> - @req.query = - auth_token: "auth-token" - @AuthenticationController.getLoggedInUser(@req, {}, @callback) - - it "should not look up the user in the database", -> - @UserGetter.getUser.called.should.equal false - - it "should return null in the callback", -> - @callback.calledWith(null, null).should.equal true - - describe "with an auth token and auth_token_allowed set to true", -> - beforeEach -> - @req.query = - auth_token: "auth-token" - @AuthenticationController.getLoggedInUser(@req, {allow_auth_token: true}, @callback) - - it "should look up the user in the database", -> - @UserGetter.getUser - .calledWith(auth_token: @req.query.auth_token) - .should.equal true - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - describe "requireLogin", -> beforeEach -> @user = _id: "user-id-123" email: "user@sharelatex.com" + @middleware = @AuthenticationController.requireLogin() - describe "when loading from the database", -> + describe "when the user is logged in", -> beforeEach -> - @middleware = @AuthenticationController.requireLogin(@options = { allow_auth_token: true, load_from_db: true }) - - describe "when the user is logged in", -> - beforeEach -> - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, @user) - @middleware(@req, @res, @next) - - it "should call getLoggedInUser with the passed options", -> - @AuthenticationController.getLoggedInUser.calledWith(@req, { allow_auth_token: true }).should.equal true - - it "should set the user property on the request", -> - @req.user.should.deep.equal @user - - it "should call the next method in the chain", -> - @next.called.should.equal true - - describe "when the user is not logged in", -> - beforeEach -> - @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, null) - @middleware(@req, @res, @next) - - it "should redirect to the register page", -> - @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true - - describe "when not loading from the database", -> - beforeEach -> - @middleware = @AuthenticationController.requireLogin(@options = { load_from_db: false }) - - describe "when the user is logged in", -> - beforeEach -> - @req.session = - user: @user = { - _id: "user-id-123" - email: "user@sharelatex.com" - } - @middleware(@req, @res, @next) - - it "should set the user property on the request", -> - @req.user.should.deep.equal @user - - it "should call the next method in the chain", -> - @next.called.should.equal true - - describe "when the user is not logged in", -> - beforeEach -> - @req.session = {} - @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - @req.query = {} - @middleware(@req, @res, @next) - - it "should redirect to the register or login page", -> - @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true - - describe "when not loading from the database but an auth_token is provided", -> - beforeEach -> - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, @user) - @middleware = @AuthenticationController.requireLogin(@options = { load_from_db: false, allow_auth_token: true }) - @req.query = auth_token: @auth_token = "auth-token-provided" + @req.session = + user: @user = { + _id: "user-id-123" + email: "user@sharelatex.com" + } @middleware(@req, @res, @next) - it "should try to load the user from the database anyway", -> - @AuthenticationController.getLoggedInUser - .calledWith(@req, {allow_auth_token: true}) - .should.equal true + it "should set the user property on the request", -> + @req.user.should.deep.equal @user + + it "should call the next method in the chain", -> + @next.called.should.equal true + + describe "when the user is not logged in", -> + beforeEach -> + @req.session = {} + @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() + @req.query = {} + @middleware(@req, @res, @next) + + it "should redirect to the register or login page", -> + @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true describe "requireGlobalLogin", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee index 3a5c60cf66..f4d5e72c26 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee @@ -95,49 +95,3 @@ describe "AuthenticationManager", -> it "should call the callback", -> @callback.called.should.equal true - - describe "getAuthToken", -> - beforeEach -> - @auth_token = "auth-token" - - describe "when the user has an auth token set", -> - beforeEach -> - @db.users.findOne = sinon.stub().callsArgWith(2, null, auth_token: @auth_token) - @AuthenticationManager.getAuthToken(@user_id, @callback) - - it "should look up the auth token in the db", -> - @db.users.findOne - .calledWith({ - _id: ObjectId(@user_id.toString()) - }, { - auth_token: true - }) - .should.equal true - - it "should return the auth token", -> - @callback.calledWith(null, @auth_token).should.equal true - - describe "when the user does not have an auth token set", -> - beforeEach -> - @db.users.findOne = sinon.stub().callsArgWith(2, null, auth_token: null) - @db.users.update = sinon.stub().callsArgWith(2, null) - @AuthenticationManager._createSecureToken = sinon.stub().callsArgWith(0, null, @auth_token) - @AuthenticationManager.getAuthToken(@user_id, @callback) - - it "should generate a new auth token", -> - @AuthenticationManager._createSecureToken.called.should.equal true - - it "should set the auth token on the user document in the db", -> - @db.users.update - .calledWith({ - _id: ObjectId(@user_id.toString()) - }, { - $set: auth_token: @auth_token - }) - .should.equal true - - it "should return the auth token", -> - @callback.calledWith(null, @auth_token).should.equal true - - - diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 84b0a250d3..32de9ebe0a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -24,52 +24,6 @@ describe "CollaboratorsController", -> @project_id = "project-id-123" @callback = sinon.stub() - describe "getCollaborators", -> - beforeEach -> - @req.params = - Project_id: @project_id - @members = [ - { - user: { _id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin", foo: "bar" } - privilegeLevel: "admin" - }, - { - user: { _id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write", foo: "bar" } - privilegeLevel: "readAndWrite" - }, - { - user: { _id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read", foo: "bar" } - privilegeLevel: "readOnly" - } - ] - @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub() - @CollaboratorsHandler.getMembersWithPrivilegeLevels - .withArgs(@project_id) - .yields(null, @members) - @res.json = sinon.stub() - @CollaboratorsController.getCollaborators(@req, @res) - - it "should return the formatted collaborators", -> - @res.json - .calledWith([ - { - id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin" - permissions: ["read", "write", "admin"] - owner: true - } - { - id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write" - permissions: ["read", "write"] - owner: false - } - { - id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read" - permissions: ["read"] - owner: false - } - ]) - .should.equal true - describe "addUserToProject", -> beforeEach -> @req.params = From 1bd8b8d1a3427f55019054d08785feed8a432da6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 17:17:26 +0000 Subject: [PATCH 23/48] Delete SecurityManager and replace with (unwritten) AuthorizationManager --- .../Authorization/AuthorizationManager.coffee | 12 ++ .../AuthorizationMiddlewear.coffee | 27 +++ .../Collaborators/CollaboratorsRouter.coffee | 6 +- .../Editor/EditorHttpController.coffee | 4 +- .../Features/Editor/EditorRouter.coffee | 18 +- .../Features/Project/ProjectController.coffee | 5 +- .../Security/AuthorizationManager.coffee | 38 ---- .../SubscriptionController.coffee | 26 +-- .../Features/Uploads/UploadsRouter.coffee | 4 +- .../coffee/managers/SecurityManager.coffee | 194 ------------------ services/web/app/coffee/router.coffee | 77 ++++--- .../Editor/EditorHttpControllerTests.coffee | 4 +- .../Project/ProjectControllerTests.coffee | 10 +- .../Security/AuthorizationManagerTests.coffee | 96 --------- .../SubscriptionControllerTests.coffee | 6 +- 15 files changed, 119 insertions(+), 408 deletions(-) create mode 100644 services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee create mode 100644 services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee delete mode 100644 services/web/app/coffee/Features/Security/AuthorizationManager.coffee delete mode 100644 services/web/app/coffee/managers/SecurityManager.coffee delete mode 100644 services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee new file mode 100644 index 0000000000..a6a80edae0 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -0,0 +1,12 @@ +module.exports = + getPrivilegeLevelForProject: (user_id, project_id, callback = (error, canAccess, privilegeLevel) ->) -> + return callback(null, true, "readAndWrite") + + canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> + + canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> + + canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> + + isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> + \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee new file mode 100644 index 0000000000..a3bce5a2cc --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -0,0 +1,27 @@ +module.exports = + ensureUserCanReadMultipleProjects: (req, res, next) -> + next() + + ensureUserCanReadProject: (req, res, next) -> + next() + + ensureUserCanWriteProjectSettings: (req, res, next) -> + next() + + ensureUserCanWriteProjectContent: (req, res, next) -> + next() + + ensureUserCanAdminProject: (req, res, next) -> + next() + + ensureUserIsSiteAdmin: (req, res, next) -> + next() + + restricted : (req, res, next)-> + if req.session.user? + res.render 'user/restricted', + title:'restricted' + else + logger.log "user not logged in and trying to access #{req.url}, being redirected to login" + res.redirect '/register' + \ No newline at end of file diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index f4f1b0343c..34a6da9a02 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -1,10 +1,10 @@ CollaboratorsController = require('./CollaboratorsController') -SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require('../Authentication/AuthenticationController') +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') module.exports = apply: (webRouter, apiRouter) -> webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject - webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject + webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject + webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 18e8f7d308..776a1c1fb0 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -5,7 +5,7 @@ EditorRealTimeController = require "./EditorRealTimeController" EditorController = require "./EditorController" ProjectGetter = require('../Project/ProjectGetter') UserGetter = require('../User/UserGetter') -AuthorizationManager = require("../Security/AuthorizationManager") +AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") @@ -34,7 +34,7 @@ module.exports = EditorHttpController = return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel) -> return callback(error) if error? if !canAccess callback null, null, false diff --git a/services/web/app/coffee/Features/Editor/EditorRouter.coffee b/services/web/app/coffee/Features/Editor/EditorRouter.coffee index 0576a4adce..9de1544875 100644 --- a/services/web/app/coffee/Features/Editor/EditorRouter.coffee +++ b/services/web/app/coffee/Features/Editor/EditorRouter.coffee @@ -1,20 +1,20 @@ EditorHttpController = require('./EditorHttpController') -SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require "../Authentication/AuthenticationController" +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') module.exports = apply: (webRouter, apiRouter) -> - webRouter.post '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc - webRouter.post '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder + webRouter.post '/project/:Project_id/doc', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.addDoc + webRouter.post '/project/:Project_id/folder', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.addFolder - webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', SecurityManager.requestCanModifyProject, EditorHttpController.renameEntity - webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', SecurityManager.requestCanModifyProject, EditorHttpController.moveEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.renameEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.moveEntity - webRouter.delete '/project/:Project_id/file/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFile - webRouter.delete '/project/:Project_id/doc/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteDoc - webRouter.delete '/project/:Project_id/folder/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFolder + webRouter.delete '/project/:Project_id/file/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFile + webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc + webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder - webRouter.post '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, EditorHttpController.restoreDoc + webRouter.post '/project/:Project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.restoreDoc # Called by the real-time API to load up the current project state. # This is a post request because it's more than just a getting of data. We take actions diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 0ca14090c2..ba5ff559f0 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -13,7 +13,7 @@ NotificationsHandler = require("../Notifications/NotificationsHandler") LimitationsManager = require("../Subscription/LimitationsManager") _ = require("underscore") Settings = require("settings-sharelatex") -SecurityManager = require("../../managers/SecurityManager") +AuthorizationManager = require("../Authorization/AuthorizationManager") fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") @@ -225,7 +225,8 @@ module.exports = ProjectController = daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)-> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel)-> + return next(error) if error? if !canAccess return res.sendStatus 401 diff --git a/services/web/app/coffee/Features/Security/AuthorizationManager.coffee b/services/web/app/coffee/Features/Security/AuthorizationManager.coffee deleted file mode 100644 index 0ec4985f35..0000000000 --- a/services/web/app/coffee/Features/Security/AuthorizationManager.coffee +++ /dev/null @@ -1,38 +0,0 @@ -SecurityManager = require '../../managers/SecurityManager' - -module.exports = AuthorizationManager = - getPrivilegeLevelForProject: ( - project, user, - callback = (error, canAccess, privilegeLevel)-> - ) -> - # This is not tested because eventually this function should be brought into - # this module. - SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel) -> - if canAccess - callback null, true, privilegeLevel - else - callback null, false - - setPrivilegeLevelOnClient: (client, privilegeLevel) -> - client.set("privilege_level", privilegeLevel) - - ensureClientCanViewProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite", "readOnly"], callback - - ensureClientCanEditProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite"], callback - - ensureClientCanAdminProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner"], callback - - ensureClientHasPrivilegeLevelForProject: (client, levels, callback = (error, project_id)->) -> - client.get "privilege_level", (error, level) -> - return callback(error) if error? - if level? - client.get "project_id", (error, project_id) -> - return callback(error) if error? - if project_id? - if levels.indexOf(level) > -1 - callback null, project_id - - diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index a2287a5d44..ba3d7b2115 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -1,4 +1,4 @@ -SecurityManager = require '../../managers/SecurityManager' +AuthenticationController = require '../Authentication/AuthenticationController' SubscriptionHandler = require './SubscriptionHandler' PlansLocator = require("./PlansLocator") SubscriptionFormatters = require("./SubscriptionFormatters") @@ -32,7 +32,7 @@ module.exports = SubscriptionController = #get to show the recurly.js page paymentPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => return next(error) if error? plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) LimitationsManager.userHasSubscription user, (err, hasSubscription)-> @@ -81,7 +81,7 @@ module.exports = SubscriptionController = userSubscriptionPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => return next(error) if error? LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) @@ -110,7 +110,7 @@ module.exports = SubscriptionController = userCustomSubscriptionPage: (req, res, next)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> res.render "subscriptions/custom_account", title: "your_subscription" @@ -118,7 +118,7 @@ module.exports = SubscriptionController = editBillingDetailsPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? LimitationsManager.userHasSubscription user, (err, hasSubscription)-> if !hasSubscription @@ -139,7 +139,7 @@ module.exports = SubscriptionController = id : user.id createSubscription: (req, res, next)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return callback(error) if error? recurly_token_id = req.body.recurly_token_id subscriptionDetails = req.body.subscriptionDetails @@ -151,14 +151,14 @@ module.exports = SubscriptionController = res.sendStatus 201 successful_subscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> res.render "subscriptions/successful_subscription", title: "thank_you" subscription:subscription cancelSubscription: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> logger.log user_id:user._id, "canceling subscription" return next(error) if error? SubscriptionHandler.cancelSubscription user, (err)-> @@ -167,7 +167,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" updateSubscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? planCode = req.body.plan_code logger.log planCode: planCode, user_id:user._id, "updating subscription" @@ -177,7 +177,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" reactivateSubscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> logger.log user_id:user._id, "reactivating subscription" return next(error) if error? SubscriptionHandler.reactivateSubscription user, (err)-> @@ -196,7 +196,7 @@ module.exports = SubscriptionController = res.sendStatus 200 renderUpgradeToAnnualPlanPage: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> planCode = subscription?.planCode.toLowerCase() if planCode?.indexOf("annual") != -1 @@ -213,7 +213,7 @@ module.exports = SubscriptionController = planName: planName processUpgradeToAnnualPlan: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> {planName} = req.body coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] annualPlanName = "#{planName}-annual" @@ -226,7 +226,7 @@ module.exports = SubscriptionController = res.sendStatus 200 extendTrial: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> SubscriptionHandler.extendTrial subscription, 14, (err)-> if err? diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index c82144acc0..fdff8d0ea3 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -1,4 +1,4 @@ -SecurityManager = require('../../managers/SecurityManager') +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') AuthenticationController = require('../Authentication/AuthenticationController') ProjectUploadController = require "./ProjectUploadController" RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') @@ -16,6 +16,6 @@ module.exports = maxRequests: 200 timeInterval: 60 * 30 }), - SecurityManager.requestCanModifyProject, + AuthorizationMiddlewear.ensureUserCanWriteProjectContent, ProjectUploadController.uploadFile diff --git a/services/web/app/coffee/managers/SecurityManager.coffee b/services/web/app/coffee/managers/SecurityManager.coffee deleted file mode 100644 index 699c307679..0000000000 --- a/services/web/app/coffee/managers/SecurityManager.coffee +++ /dev/null @@ -1,194 +0,0 @@ -logger = require('logger-sharelatex') -crypto = require 'crypto' -Assert = require 'assert' -Settings = require 'settings-sharelatex' -User = require('../models/User').User -Project = require('../models/Project').Project -ErrorController = require("../Features/Errors/ErrorController") -AuthenticationController = require("../Features/Authentication/AuthenticationController") -_ = require('underscore') -metrics = require('../infrastructure/Metrics') -querystring = require('querystring') -async = require "async" - -module.exports = SecurityManager = - restricted : (req, res, next)-> - if req.session.user? - res.render 'user/restricted', - title:'restricted' - else - logger.log "user not logged in and trying to access #{req.url}, being redirected to login" - res.redirect '/register' - - getCurrentUser: (req, callback) -> - if req.session.user? - User.findById req.session.user._id, callback - else - callback null, null - - requestCanAccessMultipleProjects: (req, res, next) -> - project_ids = req.query.project_ids?.split(",") - jobs = [] - for project_id in project_ids or [] - do (project_id) -> - jobs.push (callback) -> - # This is a bit hacky - better to have an abstracted method - # that we can pass project_id to, but this whole file needs - # a serious refactor ATM. - req.params.Project_id = project_id - SecurityManager.requestCanAccessProject req, res, (error) -> - delete req.params.Project_id - callback(error) - async.series jobs, next - - requestCanAccessProject : (req, res, next)-> - doRequest = (req, res, next) -> - getRequestUserAndProject req, res, {allow_auth_token: options?.allow_auth_token}, (err, user, project)-> - if !project? or project.archived - return ErrorController.notFound(req, res, next) - userCanAccessProject user, project, (canAccess, permissionLevel)-> - if canAccess - next() - else if user? - logger.log "user_id: #{user._id} email: #{user.email} trying to access restricted page #{req.path}" - res.redirect('/restricted') - else - logger.log "user not logged in and trying to access #{req.url}, being redirected to login" - AuthenticationController._redirectToLoginOrRegisterPage(req, res) - if arguments.length > 1 - options = - allow_auth_token: false - doRequest.apply(this, arguments) - else - options = req - return doRequest - - requestCanModifyProject : (req, res, next)-> - getRequestUserAndProject req, res, {}, (err, user, project)=> - userCanModifyProject user, project, (canModify)-> - if canModify - next() - else - logger.log "user_id: #{user?._id} email: #{user?.email} can not modify project redirecting to restricted page" - res.redirect('/restricted') - - userCanModifyProject : userCanModifyProject = (user, project, callback)-> - if !user? or !project? - callback false - else if userIsOwner user, project - callback true - else if userIsCollaberator user, project - callback true - else if project.publicAccesLevel == "readAndWrite" - callback true - else if user.isAdmin - callback true - else - callback false - - - requestIsOwner : (req, res, next)-> - getRequestUserAndProject req, res, {}, (err, user, project)-> - if !user? - return res.redirect('/restricted') - else if userIsOwner user, project || user.isAdmin - next() - else - logger.log user_id: user?._id, email: user?.email, "user is not owner of project redirecting to restricted page" - res.redirect('/restricted') - - requestIsAdmin : isAdmin = (req, res, next)-> - logger.log "checking if user is admin" - user = req.session.user - if(user? && user.isAdmin) - logger.log user: user, "User is admin" - next() - else - res.redirect('/restricted') - logger.log user:user, "is not admin redirecting to restricted page" - - userCanAccessProject : userCanAccessProject = (user, project, callback)=> - if !user? - user = {_id:'anonymous-user'} - if !project? - callback false - logger.log user:user, project:project, "Checking if can access" - if userIsOwner user, project - callback true, "owner" - else if userIsCollaberator user, project - callback true, "readAndWrite" - else if userIsReadOnly user, project - callback true, "readOnly" - else if user.isAdmin - logger.log user:user, project:project, "user is admin and can access project" - callback true, "owner" - else if project.publicAccesLevel == "readAndWrite" - logger.log user:user, project:project, "project is a public read and write project" - callback true, "readAndWrite" - else if project.publicAccesLevel == "readOnly" - logger.log user:user, project:project, "project is a public read only project" - callback true, "readOnly" - else - metrics.inc "security.denied" - logger.log user:user, project:project, "Security denied - user can not enter project" - callback false - - userIsOwner : userIsOwner = (user, project)-> - if !user? - return false - else - userId = user._id+'' - ownerRef = getProjectIdFromRef(project.owner_ref) - if userId == ownerRef - true - else - false - - userIsCollaberator : userIsCollaberator = (user, project)-> - if !user? - return false - else - userId = user._id+'' - result = false - _.each project.collaberator_refs, (colabRef)-> - colabRef = getProjectIdFromRef(colabRef) - if colabRef == userId - result = true - return result - - userIsReadOnly : userIsReadOnly = (user, project)-> - if !user? - return false - else - userId = user._id+'' - result = false - _.each project.readOnly_refs, (readOnlyRef)-> - readOnlyRef = getProjectIdFromRef(readOnlyRef) - - if readOnlyRef == userId - result = true - return result - -getRequestUserAndProject = (req, res, options, callback)-> - project_id = req.params.Project_id - if !project_id? - logger.log project_id:project_id, options:options, url:req?.url, "no project_id trying to getRequestUserAndProject" - return res.send 422 - Project.findById project_id, 'name owner_ref readOnly_refs collaberator_refs publicAccesLevel archived', (err, project)=> - if err? - logger.err err:err, "error getting project for security check" - return callback err - AuthenticationController.getLoggedInUser req, options, (err, user)=> - if err? - logger.err err:err, "error getting last logged in user for security check" - callback err, user, project - -getProjectIdFromRef = (ref)-> - if !ref? - return null - else if ref._id? - return ref._id+'' - else - return ref+'' - - diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 36b1bd7ea2..305b3bd564 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -3,8 +3,6 @@ ErrorController = require('./Features/Errors/ErrorController') ProjectController = require("./Features/Project/ProjectController") ProjectApiController = require("./Features/Project/ProjectApiController") SpellingController = require('./Features/Spelling/SpellingController') -SecurityManager = require('./managers/SecurityManager') -AuthorizationManager = require('./Features/Security/AuthorizationManager') EditorController = require("./Features/Editor/EditorController") EditorRouter = require("./Features/Editor/EditorRouter") Settings = require('settings-sharelatex') @@ -39,6 +37,7 @@ RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') InactiveProjectController = require("./Features/InactiveData/InactiveProjectController") ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') +AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') logger = require("logger-sharelatex") _ = require("underscore") @@ -54,7 +53,7 @@ module.exports = class Router webRouter.post '/login', AuthenticationController.login webRouter.get '/logout', UserController.logout - webRouter.get '/restricted', SecurityManager.restricted + webRouter.get '/restricted', AuthorizationMiddlewear.restricted # Left as a placeholder for implementing a public register page webRouter.get '/register', UserPagesController.registerPage @@ -99,13 +98,13 @@ module.exports = class Router params: ["Project_id"] maxRequests: 10 timeInterval: 60 - }), SecurityManager.requestCanAccessProject, ProjectController.loadEditor - webRouter.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile - webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings - webRouter.post '/project/:Project_id/settings/admin', SecurityManager.requestIsOwner, ProjectController.updateProjectAdminSettings + }), AuthorizationMiddlewear.ensureUserCanReadProject, ProjectController.loadEditor + webRouter.get '/Project/:Project_id/file/:File_id', AuthorizationMiddlewear.ensureUserCanReadProject, FileStoreController.getFile + webRouter.post '/project/:Project_id/settings', AuthorizationMiddlewear.ensureUserCanWriteProjectSettings, ProjectController.updateProjectSettings + webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings - webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile - webRouter.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf + webRouter.post '/project/:Project_id/compile', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.compile + webRouter.get '/Project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = @@ -113,24 +112,24 @@ module.exports = class Router "file": req.params[1] req.params = params next() - ), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi - webRouter.delete "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles - webRouter.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync - webRouter.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync - webRouter.get "/project/:Project_id/wordcount", SecurityManager.requestCanAccessProject, CompileController.wordCount + ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles + webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync + webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync + webRouter.get "/project/:Project_id/wordcount", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.wordCount - webRouter.delete '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject - webRouter.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject - webRouter.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject + webRouter.delete '/Project/:Project_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.deleteProject + webRouter.post '/Project/:Project_id/restore', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.restoreProject + webRouter.post '/Project/:Project_id/clone', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectController.cloneProject - webRouter.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject + webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject - webRouter.get "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - webRouter.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi + webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi - webRouter.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject - webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects + webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject + webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags webRouter.post '/tag', AuthenticationController.requireLogin(), TagsController.createTag @@ -174,26 +173,26 @@ module.exports = class Router webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages - webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage + webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages + webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage webRouter.get /learn(\/.*)?/, WikiController.getPage - webRouter.post "/project/:Project_id/references/index", SecurityManager.requestCanAccessProject, ReferencesController.index - webRouter.post "/project/:Project_id/references/indexAll", SecurityManager.requestCanAccessProject, ReferencesController.indexAll + webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index + webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll #Admin Stuff - webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index - webRouter.get '/admin/user', SecurityManager.requestIsAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon - webRouter.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser - webRouter.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register - webRouter.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor - webRouter.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers - webRouter.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription - webRouter.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds - webRouter.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser - webRouter.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage - webRouter.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages + webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index + webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon + webRouter.get '/admin/register', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.registerNewUser + webRouter.post '/admin/register', AuthorizationMiddlewear.ensureUserIsSiteAdmin, UserController.register + webRouter.post '/admin/closeEditor', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.closeEditor + webRouter.post '/admin/dissconectAllUsers', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.dissconectAllUsers + webRouter.post '/admin/syncUserToSubscription', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.syncUserToSubscription + webRouter.post '/admin/flushProjectToTpds', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.flushProjectToTpds + webRouter.post '/admin/pollDropboxForUser', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.pollDropboxForUser + webRouter.post '/admin/messages', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.createMessage + webRouter.post '/admin/messages/clear', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.clearMessages apiRouter.get '/perfTest', (req,res)-> res.send("hello") @@ -205,7 +204,7 @@ module.exports = class Router webRouter.get '/health_check', HealthCheckController.check webRouter.get '/health_check/redis', HealthCheckController.checkRedis - apiRouter.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) -> + apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> sendRes = _.once (statusCode, message)-> res.writeHead statusCode res.end message diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 21c8a195e4..1160c81c37 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -10,7 +10,7 @@ describe "EditorHttpController", -> '../Project/ProjectDeleter' : @ProjectDeleter = {} '../Project/ProjectGetter' : @ProjectGetter = {} '../User/UserGetter' : @UserGetter = {} - "../Security/AuthorizationManager": @AuthorizationManager = {} + "../Authorization/AuthorizationManager": @AuthorizationManager = {} '../Project/ProjectEditorHandler': @ProjectEditorHandler = {} "./EditorRealTimeController": @EditorRealTimeController = {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } @@ -119,7 +119,7 @@ describe "EditorHttpController", -> it "should check the privilege level", -> @AuthorizationManager.getPrivilegeLevelForProject - .calledWith(@project, @user) + .calledWith(@user_id, @project_id) .should.equal true it "should return the project model view, privilege level and protocol version", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 0be616da51..1dbdc8539e 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -39,8 +39,8 @@ describe "ProjectController", -> findOne: sinon.stub() @UserModel = findById: sinon.stub() - @SecurityManager = - userCanAccessProject:sinon.stub() + @AuthorizationManager = + getPrivilegeLevelForProject:sinon.stub() @EditorController = renameProject:sinon.stub() @InactiveProjectManager = @@ -66,7 +66,7 @@ describe "ProjectController", -> "../Notifications/NotificationsHandler":@NotificationsHandler '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel - "../../managers/SecurityManager":@SecurityManager + "../Authorization/AuthorizationManager":@AuthorizationManager "../InactiveData/InactiveProjectManager":@InactiveProjectManager "./ProjectUpdateHandler":@ProjectUpdateHandler "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler @@ -299,7 +299,7 @@ describe "ProjectController", -> @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, true, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -332,7 +332,7 @@ describe "ProjectController", -> @ProjectController.loadEditor @req, @res it "should not render the page if the project can not be accessed", (done)-> - @SecurityManager.userCanAccessProject = sinon.stub().callsArgWith 2, false + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, false @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee deleted file mode 100644 index 5be9c5ea20..0000000000 --- a/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee +++ /dev/null @@ -1,96 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/AuthorizationManager' -MockClient = require "../helpers/MockClient" - -describe "AuthorizationManager", -> - beforeEach -> - @client = new MockClient() - @AuthorizationManager = SandboxedModule.require modulePath, requires: - '../../managers/SecurityManager':{} - - describe "ensureClientCanViewProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should let the request through for a readOnly privilege", (done) -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should let the request through for a readAndWrite privilege", (done) -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanViewProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientCanEditProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should ignore a readOnly privilege", -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanEditProject @client, () -> - throw new Error("Should not be called") - - it "should let the request through for a readAndWrite privilege", (done) -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanEditProject @client, done - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanEditProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanEditProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientCanAdminProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should ignore a readOnly privilege", -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - it "should ignore a readAndWrite privilege", -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanAdminProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientHasPrivilegeLevelForProject", -> - it "should ignore callback if privilege_level is not set", -> - @client.set("project_id", "project-id") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - throw new Error("Should not be called") - - it "should ignore callback if project_id is not set", -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - throw new Error("Should not be called") - - it "should return the project_id", (done) -> - @client.set("privilege_level", "owner") - @client.set("project_id", "project-id-123") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - project_id.should.equal "project-id-123" - done() - diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index 87cbc11671..631e4b57b6 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -23,8 +23,8 @@ describe "SubscriptionController sanboxed", -> @user = {email:"tom@yahoo.com"} @activeRecurlySubscription = mockSubscriptions["subscription-123-active"] - @SecurityManager = - getCurrentUser: sinon.stub().callsArgWith(1, null, @user) + @AuthenticationController = + getLoggedInUser: sinon.stub().callsArgWith(1, null, @user) @SubscriptionHandler = createSubscription: sinon.stub().callsArgWith(3) updateSubscription: sinon.stub().callsArgWith(3) @@ -61,7 +61,7 @@ describe "SubscriptionController sanboxed", -> @SubscriptionDomainHandler = getDomainLicencePage:sinon.stub() @SubscriptionController = SandboxedModule.require modulePath, requires: - '../../managers/SecurityManager': @SecurityManager + '../Authentication/AuthenticationController': @AuthenticationController './SubscriptionHandler': @SubscriptionHandler "./PlansLocator": @PlansLocator './SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder From 71ef0457282fcac6a26d304b1902ae50944e4296 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 14 Mar 2016 17:06:57 +0000 Subject: [PATCH 24/48] Implement authorization guards in Authorization{Manager,Controller} --- .../AuthenticationController.coffee | 2 + .../Authorization/AuthorizationManager.coffee | 57 ++- .../AuthorizationMiddlewear.coffee | 94 ++++- .../Collaborators/CollaboratorsHandler.coffee | 18 +- .../Editor/EditorHttpController.coffee | 4 +- .../Features/Project/ProjectController.coffee | 4 +- .../Project/ProjectEditorHandler.coffee | 5 +- .../AuthorizationManagerTests.coffee | 340 ++++++++++++++++++ .../AuthorizationMiddlewearTests.coffee | 221 ++++++++++++ .../CollaboratorsHandlerTests.coffee | 22 +- .../Editor/EditorHttpControllerTests.coffee | 4 +- .../Project/ProjectControllerTests.coffee | 4 +- .../Project/ProjectEditorHandlerTests.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 4 +- 14 files changed, 753 insertions(+), 28 deletions(-) create mode 100644 services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e7db5d9f65..24bc5743f3 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -99,6 +99,7 @@ module.exports = AuthenticationController = _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" + console.log req.session req.query.redir = req.path url = "/login?#{querystring.stringify(req.query)}" res.redirect url @@ -141,4 +142,5 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser + console.log "LOGGED IN", req.session callback() diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index a6a80edae0..81d62c8b8e 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -1,12 +1,61 @@ -module.exports = - getPrivilegeLevelForProject: (user_id, project_id, callback = (error, canAccess, privilegeLevel) ->) -> - return callback(null, true, "readAndWrite") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +Project = require("../../models/Project").Project +User = require("../../models/User").User + +module.exports = AuthorizationManager = + # Get the privilege level that the user has for the project + # Returns: + # * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has + # access. false if the user does not have access + # * becausePublic: true if the access level is only because the project is public. + getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) -> + getPublicAccessLevel = () -> + Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> + return callback(error) if error? + if project.publicAccesLevel in ["readOnly", "readAndWrite"] + return callback null, project.publicAccesLevel, true + else + return callback null, false, false + if !user_id? + getPublicAccessLevel() + else + CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + if privilegeLevel? and privilegeLevel + # The user has direct access + callback null, privilegeLevel, false + else + getPublicAccessLevel() + canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite", "readOnly"]) + + canUserWriteProjectContent: (user_id, project_id, callback = (error, canWriteContent) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite"]) canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel, becausePublic) -> + return callback(error) if error? + if privilegeLevel == "owner" + return callback null, true + else if privilegeLevel == "readAndWrite" and !becausePublic + return callback null, true + else + return callback null, false canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel == "owner") isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> - \ No newline at end of file + if !user_id? + return callback null, false + User.findOne { _id: user_id }, { isAdmin: 1 }, (error, user) -> + return callback(error) if error? + return callback null, (user?.isAdmin == true) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index a3bce5a2cc..2db1632ecf 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -1,21 +1,101 @@ -module.exports = +AuthorizationManager = require("./AuthorizationManager") +async = require "async" +logger = require "logger-sharelatex" + +module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> - next() + project_ids = (req.query.project_ids or "").split(",") + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + # Remove the projects we have access to. Note rejectSeries doesn't use + # errors in callbacks + async.rejectSeries project_ids, (project_id, cb) -> + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + cb(canRead) + , (unauthorized_project_ids) -> + if unauthorized_project_ids.length > 0 + AuthorizationMiddlewear.redirectToRestricted req, res, next + else + next() ensureUserCanReadProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + if canRead + logger.log {user_id, project_id}, "allowing user read access to project" + next() + else + logger.log {user_id, project_id}, "denying user read access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectSettings: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectSettings user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project settings" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectContent: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectContent user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project content" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanAdminProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserAdminProject user_id, project_id, (error, canAdmin) -> + return next(error) if error? + if canAdmin + logger.log {user_id, project_id}, "allowing user admin access to project" + next() + else + logger.log {user_id, project_id}, "denying user admin access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserIsSiteAdmin: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> + return next(error) if error? + if isAdmin + logger.log {user_id}, "allowing user admin access to site" + next() + else + logger.log {user_id}, "denying user admin access to site" + AuthorizationMiddlewear.redirectToRestricted req, res, next + + _getUserAndProjectId: (req, callback = (error, user_id, project_id) ->) -> + project_id = req.params?.project_id or req.params?.Project_id + if !project_id? + return callback(new Error("Expected project_id in request parameters")) + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return callback(error) if error? + callback(null, user_id, project_id) + + _getUserId: (req, callback = (error, user_id) ->) -> + if req.session?.user?._id? + user_id = req.session.user._id + else + user_id = null + callback null, user_id + + redirectToRestricted: (req, res, next) -> + res.redirect "/restricted" restricted : (req, res, next)-> if req.session.user? diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index f6b8ca7bbd..4f49505af7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -13,11 +13,11 @@ module.exports = CollaboratorsHandler = return callback(error) if error? return callback null, null if !project? members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: "admin" } + members.push { id: project.owner_ref.toString(), privilegeLevel: "owner" } for member_id in project.readOnly_refs or [] - members.push { id: member_id, privilegeLevel: "readOnly" } + members.push { id: member_id.toString(), privilegeLevel: "readOnly" } for member_id in project.collaberator_refs or [] - members.push { id: member_id, privilegeLevel: "readAndWrite" } + members.push { id: member_id.toString(), privilegeLevel: "readAndWrite" } return callback null, members getMemberIds: (project_id, callback = (error, member_ids) ->) -> @@ -33,7 +33,17 @@ module.exports = CollaboratorsHandler = UserGetter.getUser member.id, (error, user) -> return cb(error) if error? return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) - callback + callback + + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> + # In future if the schema changes and getting all member ids is more expensive (multiple documents) + # then optimise this. + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + for member in members or [] + if member.id == user_id?.toString() + return callback null, member.privilegeLevel + return callback null, false getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 776a1c1fb0..ab252472bd 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -34,9 +34,9 @@ module.exports = EditorHttpController = return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !canAccess + if !privilegeLevel callback null, null, false else callback(null, diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ba5ff559f0..ae90f291b2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -225,9 +225,9 @@ module.exports = ProjectController = daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel)-> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> return next(error) if error? - if !canAccess + if !privilegeLevel return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 5b3143d765..fb4e072672 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -27,11 +27,12 @@ module.exports = ProjectEditorHandler = owner = null for member in members - if member.privilegeLevel == "admin" + if member.privilegeLevel == "owner" owner = member.user else result.members.push @buildUserModelView member.user, member.privilegeLevel - result.owner = @buildUserModelView owner, "owner" + if owner? + result.owner = @buildUserModelView owner, "owner" if owner?.features? if owner.features.collaborators? diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee new file mode 100644 index 0000000000..b1a464829c --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -0,0 +1,340 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationManager", -> + beforeEach -> + @AuthorizationManager = SandboxedModule.require modulePath, requires: + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../../models/Project": Project: @Project = {} + "../../models/User": User: @User = {} + @user_id = "user-id-1" + @project_id = "project-id-1" + @callback = sinon.stub() + + describe "getPrivilegeLevelForProject", -> + beforeEach -> + @Project.findOne = sinon.stub() + @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() + + describe "with a private project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "private" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with a public project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "readAndWrite" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "canUserReadProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal false + done() + + describe "canUserWriteProjectContent", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserWriteProjectSettings", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as a collaborator", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as the public", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", true) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserAdminProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "isUserSiteAdmin", -> + beforeEach -> + @User.findOne = sinon.stub() + + describe "when user is admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: true }) + + it "should return true", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal true + done() + + describe "when user is not admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: false }) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when user is not found", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, null) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when no user is passed", -> + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin null, (error, isAdmin) => + @User.findOne.called.should.equal false + expect(isAdmin).to.equal false + done() diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee new file mode 100644 index 0000000000..7e1ca2d5ef --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -0,0 +1,221 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddlewear.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationMiddlewear", -> + beforeEach -> + @AuthorizationMiddlewear = SandboxedModule.require modulePath, requires: + "./AuthorizationManager": @AuthorizationManager = {} + "logger-sharelatex": {log: () ->} + @user_id = "user-id-123" + @project_id = "project-id-123" + @req = {} + @res = {} + @next = sinon.stub() + + METHODS_TO_TEST = { + "ensureUserCanReadProject": "canUserReadProject" + "ensureUserCanWriteProjectSettings": "canUserWriteProjectSettings" + "ensureUserCanWriteProjectContent": "canUserWriteProjectContent" + "ensureUserCanAdminProject": "canUserAdminProject" + } + for middlewearMethod, managerMethod of METHODS_TO_TEST + do (middlewearMethod, managerMethod) -> + describe middlewearMethod, -> + beforeEach -> + @req.params = + project_id: @project_id + @AuthorizationManager[managerMethod] = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with missing project_id", -> + beforeEach -> + @req.params = {} + + it "should return an error to next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.calledWith(new Error()).should.equal true + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserIsSiteAdmin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserCanReadMultipleProjects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + @req.query = + project_ids: "project1,project2" + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 391b533cdf..f659cfebe6 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -36,7 +36,7 @@ describe "CollaboratorsHandler", -> it "should return an array of member ids with their privilege levels", -> @callback .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "admin" } + { id: "owner-ref", privilegeLevel: "owner" } { id: "read-only-ref-1", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } @@ -83,6 +83,26 @@ describe "CollaboratorsHandler", -> ]) .should.equal true + describe "getMemberIdPrivilegeLevel", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, [ + {id: "member-id-1", privilegeLevel: "readAndWrite"} + {id: "member-id-2", privilegeLevel: "readOnly"} + ]) + + it "should return the privilege level if it exists", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-2", @project_id, (error, level) -> + expect(level).to.equal "readOnly" + done() + + it "should return false if the member has no privilege level", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> + expect(level).to.equal false + done() + describe "isUserMemberOfProject", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 1160c81c37..6a594f31ee 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -99,7 +99,7 @@ describe "EditorHttpController", -> describe "when authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, true, "owner") + sinon.stub().callsArgWith(2, null, "owner") @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should find the project without doc lines", -> @@ -128,7 +128,7 @@ describe "EditorHttpController", -> describe "when not authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, false, null) + sinon.stub().callsArgWith(2, null, null) @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should return false in the callback", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 1dbdc8539e..54ea831e05 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -299,7 +299,7 @@ describe "ProjectController", -> @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -332,7 +332,7 @@ describe "ProjectController", -> @ProjectController.loadEditor @req, @res it "should not render the page if the project can not be accessed", (done)-> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, false + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, null @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 7d71f6c89f..eede50bfd3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -49,7 +49,7 @@ describe "ProjectEditorHandler", -> last_name : "ShareLaTeX" email : "owner@sharelatex.com" }, - privilegeLevel: "admin" + privilegeLevel: "owner" },{ user: { _id: "read-only-id" diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index a9533e9533..bed1a56f29 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -44,6 +44,8 @@ class User projectName: name }, (error, response, body) -> return callback(error) if error? + if !body?.project_id? + console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body callback(null, body.project_id) addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> @@ -240,7 +242,7 @@ describe "Authorization", -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @anon, @project_id, redirect_to: "/login", done + expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done From b556d57f40d74ac904a7dcc644895b1e88b4cf01 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 14 Mar 2016 17:11:23 +0000 Subject: [PATCH 25/48] Remove missed console.log debugging lines in AuthenticationController.coffee --- .../Features/Authentication/AuthenticationController.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 24bc5743f3..e7db5d9f65 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -99,7 +99,6 @@ module.exports = AuthenticationController = _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" - console.log req.session req.query.redir = req.path url = "/login?#{querystring.stringify(req.query)}" res.redirect url @@ -142,5 +141,4 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser - console.log "LOGGED IN", req.session callback() From 57818944537e4b36a6d54971eab16acc9429010b Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:05:59 +0000 Subject: [PATCH 26/48] Do array null check in callback args --- .../Collaborators/CollaboratorsHandler.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 4f49505af7..3f76a6bedb 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -26,9 +26,9 @@ module.exports = CollaboratorsHandler = return callback null, members.map (m) -> m.id getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - async.mapLimit (members or []), 3, + async.mapLimit members, 3, (member, cb) -> UserGetter.getUser member.id, (error, user) -> return cb(error) if error? @@ -38,9 +38,9 @@ module.exports = CollaboratorsHandler = getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> # In future if the schema changes and getting all member ids is more expensive (multiple documents) # then optimise this. - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - for member in members or [] + for member in members if member.id == user_id?.toString() return callback null, member.privilegeLevel return callback null, false @@ -56,9 +56,9 @@ module.exports = CollaboratorsHandler = return callback null, count - 1 # Don't count project owner isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - for member in members or [] + for member in members if member.id.toString() == user_id.toString() return callback null, true, member.privilegeLevel return callback null, false, null From d09705142e9b86cfd6654aa50744a81492687af4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:07:34 +0000 Subject: [PATCH 27/48] Add in missing error checks --- .../coffee/Features/Collaborators/CollaboratorsHandler.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 3f76a6bedb..7f0cd6ebca 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -65,8 +65,10 @@ module.exports = CollaboratorsHandler = getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> + return callback(err) if err? Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> - callback(err, readAndWriteProjects, readOnlyProjects) + return callback(err) if err? + callback(null, readAndWriteProjects, readOnlyProjects) removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" From fe1f71413e09a25c6ec2e57706002e48485aea67 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:12:43 +0000 Subject: [PATCH 28/48] Use ProjectGetter, not Project, in ProjectController.loadEditor --- .../app/coffee/Features/Project/ProjectController.coffee | 3 +-- .../UnitTests/coffee/Project/ProjectControllerTests.coffee | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ae90f291b2..4e1a6acf2c 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -5,7 +5,6 @@ projectDuplicator = require("./ProjectDuplicator") projectCreationHandler = require("./ProjectCreationHandler") editorController = require("../Editor/EditorController") metrics = require('../../infrastructure/Metrics') -Project = require('../../models/Project').Project User = require('../../models/User').User TagsHandler = require("../Tags/TagsHandler") SubscriptionLocator = require("../Subscription/SubscriptionLocator") @@ -195,7 +194,7 @@ module.exports = ProjectController = async.parallel { project: (cb)-> - Project.findOne { _id: project_id }, cb + ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb user: (cb)-> if user_id == 'openUser' cb null, defaultSettingsForAnonymousUser(user_id) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 54ea831e05..4db3648988 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -35,8 +35,6 @@ describe "ProjectController", -> getAllTags: sinon.stub() @NotificationsHandler = getUserNotifications: sinon.stub() - @ProjectModel = - findOne: sinon.stub() @UserModel = findById: sinon.stub() @AuthorizationManager = @@ -51,6 +49,7 @@ describe "ProjectController", -> indexProjectReferences: sinon.stub() @ProjectGetter = findAllUsersProjects: sinon.stub() + getProject: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -64,7 +63,6 @@ describe "ProjectController", -> "../Subscription/LimitationsManager": @LimitationsManager "../Tags/TagsHandler":@TagsHandler "../Notifications/NotificationsHandler":@NotificationsHandler - '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel "../Authorization/AuthorizationManager":@AuthorizationManager "../InactiveData/InactiveProjectManager":@InactiveProjectManager @@ -296,7 +294,7 @@ describe "ProjectController", -> fontSize:"massive" theme:"sexy" email: "bob@bob.com" - @ProjectModel.findOne.callsArgWith 1, null, @project + @ProjectGetter.getProject.callsArgWith 2, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" From 724e6b52634d887b5db163058badffac7ca9f098 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:14:33 +0000 Subject: [PATCH 29/48] Require explicit value of true for ENV variables in config --- services/web/config/settings.defaults.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index b45837193b..ffa91e31b9 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -256,7 +256,7 @@ module.exports = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. - allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"]? then true else false + allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb From 398d43e2d1cba21d89a3c6862dbfff76f89e9782 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:15:25 +0000 Subject: [PATCH 30/48] Add missing ? check --- .../coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 88397c9ef6..747a763e51 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -121,7 +121,7 @@ getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> return callback(err) if err? CollaboratorsHandler.getMemberIds project_id, (err, member_ids) -> return callback(err) if err? - callback err, project.owner_ref, member_ids + callback err, project?.owner_ref, member_ids mergeProjectNameAndPath = (project_name, path)-> if(path.indexOf('/') == 0) From 261466b042bc616b65028426526d984708e28909 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:35:01 +0000 Subject: [PATCH 31/48] Convert privilege levels to an enum --- .../Authorization/AuthorizationManager.coffee | 22 +++++++++++-------- .../Authorization/PrivilegeLevels.coffee | 5 +++++ .../Authorization/PublicAccessLevels.coffee | 4 ++++ .../Collaborators/CollaboratorsHandler.coffee | 13 ++++++----- .../Features/Project/ProjectController.coffee | 3 ++- .../Project/ProjectDetailsHandler.coffee | 3 ++- 6 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee create mode 100644 services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index 81d62c8b8e..db49881bbf 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -1,6 +1,8 @@ CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") Project = require("../../models/Project").Project User = require("../../models/User").User +PrivilegeLevels = require("./PrivilegeLevels") +PublicAccessLevels = require("./PublicAccessLevels") module.exports = AuthorizationManager = # Get the privilege level that the user has for the project @@ -12,17 +14,19 @@ module.exports = AuthorizationManager = getPublicAccessLevel = () -> Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> return callback(error) if error? - if project.publicAccesLevel in ["readOnly", "readAndWrite"] - return callback null, project.publicAccesLevel, true + if project.publicAccesLevel == PublicAccessLevels.READ_ONLY + return callback null, PrivilegeLevels.READ_ONLY + else if project.publicAccesLevel == PublicAccessLevels.READ_AND_WRITE + return callback null, PrivilegeLevels.READ_AND_WRITE, true else - return callback null, false, false + return callback null, PrivilegeLevels.NONE, false if !user_id? getPublicAccessLevel() else CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if privilegeLevel? and privilegeLevel + if privilegeLevel? and privilegeLevel != PrivilegeLevels.NONE # The user has direct access callback null, privilegeLevel, false else @@ -31,19 +35,19 @@ module.exports = AuthorizationManager = canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel in ["owner", "readAndWrite", "readOnly"]) + return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.READ_ONLY]) canUserWriteProjectContent: (user_id, project_id, callback = (error, canWriteContent) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel in ["owner", "readAndWrite"]) + return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE]) canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel, becausePublic) -> return callback(error) if error? - if privilegeLevel == "owner" + if privilegeLevel == PrivilegeLevels.OWNER return callback null, true - else if privilegeLevel == "readAndWrite" and !becausePublic + else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE and !becausePublic return callback null, true else return callback null, false @@ -51,7 +55,7 @@ module.exports = AuthorizationManager = canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel == "owner") + return callback null, (privilegeLevel == PrivilegeLevels.OWNER) isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> if !user_id? diff --git a/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee b/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee new file mode 100644 index 0000000000..682ae08a02 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee @@ -0,0 +1,5 @@ +module.exports = + NONE: false + READ_ONLY: "readOnly" + READ_AND_WRITE: "readAndWrite" + OWNER: "owner" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee b/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee new file mode 100644 index 0000000000..8e63a64a33 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee @@ -0,0 +1,4 @@ +module.exports = + READ_ONLY: "readOnly" + READ_AND_WRITE: "readAndWrite" + PRIVATE: "private" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 7f0cd6ebca..cb6459c85f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -6,6 +6,7 @@ UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" +PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> @@ -13,11 +14,11 @@ module.exports = CollaboratorsHandler = return callback(error) if error? return callback null, null if !project? members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: "owner" } + members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } for member_id in project.readOnly_refs or [] - members.push { id: member_id.toString(), privilegeLevel: "readOnly" } + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY } for member_id in project.collaberator_refs or [] - members.push { id: member_id.toString(), privilegeLevel: "readAndWrite" } + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } return callback null, members getMemberIds: (project_id, callback = (error, member_ids) ->) -> @@ -43,7 +44,7 @@ module.exports = CollaboratorsHandler = for member in members if member.id == user_id?.toString() return callback null, member.privilegeLevel - return callback null, false + return callback null, PrivilegeLevels.NONE getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> @@ -100,10 +101,10 @@ module.exports = CollaboratorsHandler = if existing_users.indexOf(user_id.toString()) > -1 return callback null # User already in Project - if privilegeLevel == 'readAndWrite' + if privilegeLevel == PrivilegeLevels.READ_AND_WRITE level = {"collaberator_refs":user_id} logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user" - else if privilegeLevel == 'readOnly' + else if privilegeLevel == PrivilegeLevels.READ_ONLY level = {"readOnly_refs":user_id} logger.log {privileges: "readOnly", user_id, project_id}, "adding user" else diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 4e1a6acf2c..d6a3e69ce4 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -17,6 +17,7 @@ fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") ProjectGetter = require("./ProjectGetter") +PrivilegeLevels = require("../Authorization/PrivilegeLevels") module.exports = ProjectController = @@ -226,7 +227,7 @@ module.exports = ProjectController = AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> return next(error) if error? - if !privilegeLevel + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index a0f8cca509..80eb67d88e 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -4,6 +4,7 @@ Project = require('../../models/Project').Project logger = require("logger-sharelatex") tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender' _ = require("underscore") +PublicAccessLevels = require("../Authorization/PublicAccessLevels") module.exports = @@ -49,6 +50,6 @@ module.exports = setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> logger.log project_id: project_id, level: newAccessLevel, "set public access level" - if project_id? && newAccessLevel? and _.include ['readOnly', 'readAndWrite', 'private'], newAccessLevel + if project_id? && newAccessLevel? and _.include [PublicAccessLevels.READ_ONLY, PublicAccessLevels.READ_AND_WRITE, PublicAccessLevels.PRIVATE], newAccessLevel Project.update {_id:project_id},{publicAccesLevel:newAccessLevel}, (err)-> callback() \ No newline at end of file From b7d226f43476c0db8bed936b4474fcef76994a34 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:39:27 +0000 Subject: [PATCH 32/48] Make privilege level check in EditorHttpController more explicit --- .../web/app/coffee/Features/Editor/EditorHttpController.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index ab252472bd..ebe1351110 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = EditorHttpController = joinProject: (req, res, next) -> @@ -36,7 +37,7 @@ module.exports = EditorHttpController = return callback(error) if error? AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !privilegeLevel + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE callback null, null, false else callback(null, From 75d9912449b69d656d8cc2171ed6f973bcebc345 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:44:06 +0000 Subject: [PATCH 33/48] Use _.defaults to simplify assigning default features --- .../Project/ProjectEditorHandler.coffee | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index fb4e072672..a3d8319424 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -15,15 +15,6 @@ module.exports = ProjectEditorHandler = deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs members: [] - - result.features = # defaults - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false owner = null for member in members @@ -34,21 +25,15 @@ module.exports = ProjectEditorHandler = if owner? result.owner = @buildUserModelView owner, "owner" - if owner?.features? - if owner.features.collaborators? - result.features.collaborators = owner.features.collaborators - if owner.features.versioning? - result.features.versioning = owner.features.versioning - if owner.features.dropbox? - result.features.dropbox = owner.features.dropbox - if owner.features.compileTimeout? - result.features.compileTimeout = owner.features.compileTimeout - if owner.features.compileGroup? - result.features.compileGroup = owner.features.compileGroup - if owner.features.templates? - result.features.templates = owner.features.templates - if owner.features.references? - result.features.references = owner.features.references + result.features = _.defaults(owner?.features or {}, { + collaborators: -1 # Infinite + versioning: false + dropbox:false + compileTimeout: 60 + compileGroup:"standard" + templates: false + references: false + }) return result From f3db1146547d5487fde52a19e5089ca604306977 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 Mar 2016 11:20:33 +0000 Subject: [PATCH 34/48] Use the mongojs based apis to get Project and User information. --- .../Features/References/ReferencesHandler.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index b64600fdb1..bd31345f87 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -1,8 +1,8 @@ logger = require("logger-sharelatex") request = require("request") settings = require("settings-sharelatex") -Project = require("../../models/Project").Project -User = require("../../models/User").User +ProjectGetter = require "../Project/ProjectGetter" +UserGetter = require "../User/UserGetter" DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') U = require('underscore') Async = require('async') @@ -32,12 +32,12 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - User.findOne { _id: project.owner_ref }, { features: true }, (err, owner) -> + UserGetter.getUser project.owner_ref, { features: true }, (err, owner) -> return callback(err) if err? callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.findOne { _id: projectId }, (err, project) -> + ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -46,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.findOne { _id: projectId }, (err, project) -> + ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) From 35b5d3cc0581961f3c3721f4bda528b7bc5dcd86 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 Mar 2016 11:43:39 +0000 Subject: [PATCH 35/48] Update ReferencesHandlerTests. --- .../References/ReferencesHandlerTests.coffee | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 1666a2def1..53e064d821 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -39,13 +39,11 @@ describe 'ReferencesHandler', -> get: sinon.stub() post: sinon.stub() } - '../../models/Project': { - Project: @Project = { - findOne: sinon.stub().callsArgWith(1, null, @fakeProject) - } + '../Project/ProjectGetter': @ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, @fakeProject) } - '../../models/User': { - User: @User = {} + '../User/UserGetter': @UserGetter = { + getUser: sinon.stub() } '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null) @@ -73,10 +71,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.findOne', (done) -> + it 'should call ProjectGetter.getProject', (done) -> @call (err, data) => - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith(_id: @projectId).should.equal true + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -112,10 +110,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findOne produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -132,7 +130,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -150,7 +148,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -170,7 +168,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -185,7 +183,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -237,10 +235,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findOne produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -257,7 +255,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -275,7 +273,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -319,8 +317,8 @@ describe 'ReferencesHandler', -> @owner = features: references: false - @User.findOne = sinon.stub() - @User.findOne.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) + @UserGetter.getUser = sinon.stub() + @UserGetter.getUser.withArgs(@owner_ref, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback From 88b8ce1f80bd0a47884945d1da8cde4410718421 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 18 Mar 2016 12:23:13 +0000 Subject: [PATCH 36/48] Enable working settings acceptance tests --- .../web/test/acceptance/coffee/AuthorizationTests.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index bed1a56f29..0cacffb5cf 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -300,8 +300,8 @@ describe "Authorization", -> it "should allow a user write access to its content", (done) -> expect_content_write_access @other1, @project_id, done - it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow a user write access to its settings", (done) -> + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done @@ -331,8 +331,8 @@ describe "Authorization", -> it "should not allow a user write access to its content", (done) -> expect_no_content_write_access @other1, @project_id, done - it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow a user write access to its settings", (done) -> + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done From e7d67668e9a673bd4eee987cdb33900730ce3357 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 18 Mar 2016 15:59:03 +0000 Subject: [PATCH 37/48] Improve error reporting and show 404 when project ids are malformed --- services/web/app.coffee | 12 --- .../Authorization/AuthorizationManager.coffee | 5 +- .../AuthorizationMiddlewear.coffee | 4 + .../Collaborators/CollaboratorsHandler.coffee | 3 +- .../Features/Errors/ErrorController.coffee | 23 ++++- .../app/coffee/Features/Errors/Errors.coffee | 9 ++ .../app/coffee/infrastructure/Server.coffee | 4 + services/web/app/views/general/500.jade | 16 ++++ services/web/public/img/lion-sad-128.png | Bin 0 -> 14098 bytes .../AuthorizationManagerTests.coffee | 12 +++ .../AuthorizationMiddlewearTests.coffee | 16 ++++ .../CollaboratorsHandlerTests.coffee | 48 ++++++---- .../coffee/AuthorizationTests.coffee | 83 +----------------- .../acceptance/coffee/ProjectCRUDTests.coffee | 21 +++++ .../acceptance/coffee/helpers/User.coffee | 74 ++++++++++++++++ .../acceptance/coffee/helpers/request.coffee | 5 ++ 16 files changed, 222 insertions(+), 113 deletions(-) create mode 100644 services/web/app/coffee/Features/Errors/Errors.coffee create mode 100644 services/web/app/views/general/500.jade create mode 100644 services/web/public/img/lion-sad-128.png create mode 100644 services/web/test/acceptance/coffee/ProjectCRUDTests.coffee create mode 100644 services/web/test/acceptance/coffee/helpers/User.coffee create mode 100644 services/web/test/acceptance/coffee/helpers/request.coffee diff --git a/services/web/app.coffee b/services/web/app.coffee index 3d45307e5d..8496af8f17 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -18,18 +18,6 @@ argv = require("optimist") .usage("Usage: $0") .argv -Server.app.use (error, req, res, next) -> - if error?.code is 'EBADCSRFTOKEN' - logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" - res.sendStatus(403) - return - logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" - res.statusCode = error.status or 500 - if res.statusCode == 500 - res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at #{Settings.adminEmail}") - else - res.end() - if Settings.catchErrors process.removeAllListeners "uncaughtException" process.on "uncaughtException", (error) -> diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index db49881bbf..eb6f564fea 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -3,6 +3,7 @@ Project = require("../../models/Project").Project User = require("../../models/User").User PrivilegeLevels = require("./PrivilegeLevels") PublicAccessLevels = require("./PublicAccessLevels") +Errors = require("../Errors/Errors") module.exports = AuthorizationManager = # Get the privilege level that the user has for the project @@ -14,8 +15,10 @@ module.exports = AuthorizationManager = getPublicAccessLevel = () -> Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> return callback(error) if error? + if !project? + return callback new Errors.NotFoundError("no project found with id #{project_id}") if project.publicAccesLevel == PublicAccessLevels.READ_ONLY - return callback null, PrivilegeLevels.READ_ONLY + return callback null, PrivilegeLevels.READ_ONLY, true else if project.publicAccesLevel == PublicAccessLevels.READ_AND_WRITE return callback null, PrivilegeLevels.READ_AND_WRITE, true else diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index 2db1632ecf..4888db0c8a 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -1,6 +1,8 @@ AuthorizationManager = require("./AuthorizationManager") async = require "async" logger = require "logger-sharelatex" +ObjectId = require("mongojs").ObjectId +Errors = require "../Errors/Errors" module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> @@ -83,6 +85,8 @@ module.exports = AuthorizationMiddlewear = project_id = req.params?.project_id or req.params?.Project_id if !project_id? return callback(new Error("Expected project_id in request parameters")) + if !ObjectId.isValid(project_id) + return callback(new Errors.NotFoundError("invalid project_id: #{project_id}")) AuthorizationMiddlewear._getUserId req, (error, user_id) -> return callback(error) if error? callback(null, user_id, project_id) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index cb6459c85f..71737eecff 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -7,12 +7,13 @@ ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" +Errors = require "../Errors/Errors" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? - return callback null, null if !project? + return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project? members = [] members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } for member_id in project.readOnly_refs or [] diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index d0589ba5ed..a0ce6c85c9 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -1,5 +1,24 @@ +Errors = require "./Errors" +logger = require "logger-sharelatex" + module.exports = ErrorController = notFound: (req, res)-> - res.statusCode = 404 + res.status(404) res.render 'general/404', - title: "page_not_found" \ No newline at end of file + title: "page_not_found" + + serverError: (req, res)-> + res.status(500) + res.render 'general/500', + title: "Server Error" + + handleError: (error, req, res, next) -> + if error?.code is 'EBADCSRFTOKEN' + logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" + res.sendStatus(403) + return + logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" + if error instanceof Errors.NotFoundError + ErrorController.notFound req, res + else + ErrorController.serverError req, res \ No newline at end of file diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee new file mode 100644 index 0000000000..0bbff1f19b --- /dev/null +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -0,0 +1,9 @@ +NotFoundError = (message) -> + error = new Error(message) + error.name = "NotFoundError" + error.__proto__ = NotFoundError.prototype + return error +NotFoundError.prototype.__proto__ = Error.prototype + +module.exports = Errors = + NotFoundError: NotFoundError \ No newline at end of file diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 4a035ba007..fea8752bb2 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -30,6 +30,8 @@ OldAssetProxy = require("./OldAssetProxy") translations = require("translations-sharelatex").setup(Settings.i18n) Modules = require "./Modules" +ErrorController = require "../Features/Errors/ErrorController" + metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger) @@ -136,6 +138,8 @@ app.use(webRouter) router = new Router(webRouter, apiRouter) +app.use ErrorController.handleError + module.exports = app: app server: server diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade new file mode 100644 index 0000000000..12983b923b --- /dev/null +++ b/services/web/app/views/general/500.jade @@ -0,0 +1,16 @@ +extends ../layout + +block content + .content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h2 Oh dear, something went wrong. + p: img(src="/img/lion-sad-128.png", alt="Sad Lion") + p + | Something went wrong with your request, sorry. Our staff are probably looking into this, but it continues, please contact us at #{settings.adminEmail} + p + a(href="/") + i.fa.fa-arrow-circle-o-left + | #{translate("take_me_home")} diff --git a/services/web/public/img/lion-sad-128.png b/services/web/public/img/lion-sad-128.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e58c42b0e574cb98c55f4b141cad01eff6f6d4 GIT binary patch literal 14098 zcmZ{L1ymf%w)WsKxVyU!?iM__yK8WFcXxLJ0fI{i?oM!r;O_4J=bU@rd*{FFy;-ZP zyLNqFeS7b!-D~wsPo$!}Bq9(G2mk;OKTC-z|2YTzb-+UZY5N3?M*p0^oRuX-0M!!& zM}InS4pLgq002DtUk4Z+EW5WoF{?kM;VqYJcy35hDxGKg|4~{}s*uXD{%|*_&FLdx{ykmw@6MBBxzkcviRPw!C**59XF?x5{yZPF53And6U}S{ z+Yc$KDRk(|-pAM3%n>pl#2R{#6B5_}r@#!Dx@lbrDZkfV` zl2Qb0bdv38GoMJ3$^VR?j|;tW1wm3%MeL_QjPmbky8t<1d%NJa-!&w2Y&em( zhw^_BM{WAOVAweI%JCKff@_c4G|b20B43;Xl=hDa+eG!B}}IK|qnOemwn+?pbvLDj)MV*sX9VXq?%y zzR&vD$WpZC57_YVe8Y1-AO$6su`QCKPv5v=E?f}AzVj05#B)|mX_(kDO;AI`wavNa z5|Gc=4sc!$DgIs`2U)auf7Ojd7&B!P?yjs|azZW4#~c^Aus^G>&VQ zvZ8<-_zgblG-*2t?JqHGLDWuHO5yRyN=?4w!)`@&T^d+T5Q1;0xX@c~17`-A25Pt6 zyNBIRMwiY5(OMl?Bagj6(tS7==pck)h)pHcIapPj{XA)&biq7WYueq%%$0pVM^w&Q zPQ5}V9eSc?Zpg4~w0MD9x5wM+pu=fkDrW$+_mCn;TYoAQ?-SA|o9UQ}x?-cQE+f!$ zLr)q^4r30I3izT~0MR#A1M#){?Z)1qqlY5y)jlK$wfyDh#dz+C)F$f(hYv+V8 zf>7~hi#PZazV2ynfNnMA4s23H?+C2e2q4r)1)$P$vA6+^_<|05z%jRda7&`!XHC5z z?I@mT=#fwzXLt1FEFlh^ur(=z*`$4UckfG;{uN63lbyQ$g&ww#n{T{%%%%cEq-6zO zjQi9{7j%iQUS^4IKe9Kk)%^E8W*wvY1_3=ny?W;zCQH8GgPxgNJlDAD+@Zbk#e*a_ z4%(|&G>#jE{{DFKb+-KyG&4zV#rn$Nh_>LRB?WL=hR3k%1?2H%;j3+sHpy3FcNsGE+^%V+W;gu z-WkL!wy&^Io|%d+#?0D)84$f=+-h`4m(j5lcm8{EY3pL=55t`7=@-u0Y&iS~N^)(5 zTdNDM(32AR4Bo-EEsa0G%n+5FKfgZkxTId7sBULxy;9iZWv^gVO%9 zNTby|xP-(%F{0-J3{tXRDRFjR|E(0h)KF>zAqxB#!JJEFm82KXW#95IIoaeC2$-hR zw74BxeQ*I0l;6)0%e`ZQk@6R)wYG^R1D0|WY;*TY3 zRt;)G%QF!i!ZfnrMoWHU_RI|&Hor*)a@VL3k8+S*jnFD5=x0~R{f*FOUhtLr*yn}c<4jqW2D>JN}s zGMv$_qE)m8PFGMBZIQuB*M0hnV+iz_nGV0b@{c_84A|gscP?ozoJul@pmFte()kFYC~%q2&}OF4&zPM- z-a?tnnaXVIu&$E}9d!Kuoro4i)bd#zs0;l~^tNmQ`O*W?(u+>t)EcY36`Z+?S<*&m zXjdkpe@$TATjNFEo7gI|bgBb?MI*NF(?>CP5Qt>J;lazvd-;=dQWM@ODys+KamGYe zl_Q$B?oF|v*m`q~;%ai#m|`%I`>_2=Q$Kpk*HnEQEQ3Y2kWaWeaA2d#I-Mz3&h(q+ zfdC3&u~ffo`AoE@D`yQIZB&0%2SkYbNV zVrW<$D$0LBu8p~5qlHEH{c-?irXCWh3dyP}bLba{%IA(WLLyoI%<>kTsHmkuyHgSi zA3PsZlla42q1U5^k9F%-44e{kU>!OIl|x6yS~x#W(3yKg#R4rvrndvXNk(!sl9}NK z5yKA*8Oeb)#VuVOO85lXlU;@iX;RchDb6!?<=)t)M9%_ic+mc|_(+QV=uy0a=Lo;N z;yYvdZ#WJs#6j6=d^$W_jz($xfhRt*O6)q%EoM@wLgP)O5;}ySjO^RtcXzjP)>?um zlTjAp%gG*@Hklgm=JT3VIFjJr$-(9`_o3$99WP=6lIcVP^eEW87GQ~k+Tnngw2Dgj zy^6bf?i3!vklBIhXZ^%XhUaj)B9sEKaZc&2vd&1HiomGPdtV7*ZUw{rp}IMHhO2Pp zSI`)<7e135en3b)5tNa_d!UDm5(3k7I>y`ZQa{-+c#7gg{nin-%8k-v5anjO4Aahhhs75e zz}CQQh>4eGt=a+!4rXb|8>QDnvJWLNgd(>Q;YT=o(dC^V}w$bUt z8)mPn>3%DYER}xS$@z!re#|thnbXg8t(e_gVqPSu-?w(b?Oh$Tz%PCH-fCa>$ExF% zE3c20m&<@CP(*RH<1XJ~@%+H)1E($GQ-KUz@#pqwMTl}}u%7O`n94@}PCfN^S6qbr z8LFNmlRin`pYrq9HtsYu=JtcnWj=p&p2gPh6-EBM-TZQuwd#U?Un3`Hn6GU;iR* z9NFrlZkUio(0Y1WUV<#-ai7Pt*_ejp*HYH9RJYXnUg2o}6pONtrVQbO}8T`(Wv0E-U)L$&yPJM>F7<(@}<cF#>ijC@Ps#YC3Iq*O-E*m4?nr%THWMaM^9`ks< z`lq6Ln9>xEQxSwjXFR9fO6Nf-r2A6^@u8>QK!Ne!>@_2UXdXb@FQSc-X)LG`a6w-c z=PPz{O-9{YDkQTS4|6lw=igI9gV<&QOC8ePFq*`Rm|!#a6Dz0ISV4A%F-NAY!6SN( zmLFVJGcztl-f&-;XhCA59l3%80lR4&Raiufi)MCGvrq%wqsxJdsMF0A-iYsYtl=`vR1Vkaw}`7w68f&{fZ<;P|yx)TO> zP<=#VvM zYS+(-u1+8Zab@#1!bLuIY0s1XV0voS^&YY}J^s0oWDDbwFiTSjJ*GgcN&tO0HU5YCbJEZa`}UU?Zyl$|A*(M1I|_k@bY1TWV1u~ZBbei& zg$B3`$tl*uJ4*(N87HZ+cL>VmJ=2W|?NNIkS99^hOHfV2=fTvbLp_JK3rjOs+u8Sb z!-?dQ%ooE+W97Klm+-b$sP}|n0>a`1Y1=Kvl?R?FF}?HtC>CFuz#ET<)~KvPE<#1o zo<;aqkEEj&J_D`Wyua#hLg&= z3!rUovK8wdNN8$fwMXRct|l*;{>P9cM)(kpopu7~4s&{i3RNv)6GzX$=&-v!KBUgo z%|j*Y%0%17C(R|Xgc&bvCM2iWU5^YtGu;k5KCEHxDHSSk=#nf#r!aA-g6Pz!zA0N- zHHSJHl#i-s6QGGKzj$tkB>gFl{HNJ8IDUcVYUO@f0)YT(5(|otzFT|aAR|?m|GDid zPs60U@~U(KxFlyVIS>UIUe!Y7-TJ)JY%12{sZ1K5C>+gZV1og{2mOWHfOUV?gyKnL z@UiGOS3)JfDDjhZCd-?ua_jPXo#@+yzozGnDmZlI?QE0=lNp+qXbAvYm$uiKIg=c=vB)2iudo7id zlF;wly^A5t1RwUV`NMGqm7CARk>TIrZ=WNc;zMO9I_)QomTR#lIq$(Fq))$1K`7_Z zv@;g_^z^}e<$0(|IrCL4rXj7u+Pr^KM(WvdC{i&UsoyDd8v2!&sjqne28@Axw8&Qp zfBX0fA0pG_?Y~BLae9Fh%+s6>5h!DC?sNl%rkZjGwoOl4kDn5C;9(>m z(g@{1mn87ynH3 z&N>L+F+~`daOrFCS+ypvHW}w)92VBpQThU?V)k%HA$DF;C>44z+Z@p^sDZ|qyb$3cX zKNIG>?RfIS+;BFX64o{1a<%0-UilWYC ztSMSoPK7Cf7J*wi82v|?%Ym`;LH0+L*!p0|WhmDb5Ks15kXb+yD?gdMNE)4h9L+{i zNvYcIT&x>W?%VnfLcPz}CBTy8>cc(a-SeBxvq)b(plLicK0HVs0@e!`2H!5URx(*u zU)A>P3@>uQ3Et$b7D^a=Rx=Jp7@BWz+$zbnWofGAveJgfzA$soIUF?KYmwa?C@$$W zhCIJERbpBG9-)&Zut5`WWAU*g^7$MPVyy6Pp$WX)Jh`U$-nM*itX=~thH5fXn379C z665}qJN~ym|Qz#c`%);-=3rLuCkYl?CmLFb%vm z5VUDi+!nPwu0KqnBH+$;}ac z%yXiHn%eFaNa*wkgHwGh!*Ax?B$~8OrXZBrA5{~u%lqVH-FhS!30|9sT%EXg*mJP; z)=Y0xqe0)D7wg0J!UZ{lpovp63lUZUp=*UF_Uo7Y1!(~PmWc_S$CQ{?8Mwp}EUA_9 zFu{!6Gh$6;{O4`Grio;h{#2%PRwzRVVo$w$!FKtyfM$Bft@W_E_PneSMC{7m*j*Fk)=+nOI<52-$ zG0+hIaoEdqE7A3OOEFEhd#&k2czRJ7c>;WjHuaSwPH7`wPH>Gn@LN$m8W6CdK(8T* zQ7gypX#|TEe8W&2wiaENeg7SL_A(g@s`kvsCH2(bCHNJ|o#k?kF;n(zfhT~q`{W7G zh;WsZ`VQdQQ*g%iLrE2(&TYRe>R8nl-gTT4eV%}g*+Z-fyxv03&!-oXf#D15KxNk~ z5_Avr0Z;h|p7%I$hGnivV8g+&AktDx?8dq@yvw6|^7Qa%=pYOirO`X7v^%LXqQIN+ z4_dFo&HwrYYpRbNx@mIf;haV=_@%CABq9IzcG>N0IRK|^Fem`#twFMP5N{Jg;TK?( zbpK?nK~>ok%@GrGPZ+Fq8ho~*Y_kD;kgC>5;u&^T{l};BG`I)#SEgPTf2;CdOG0qr z-xgDfz7ct*kDu#|va6Ft;u$^H-~2n(azN>v=@o(s7nj>B!k!xfuy)tm$E2)*tgg$v zg>*y_&RgIRV3g^l3eeuY7JLoB$x^bA6$a&XW=X+Zg8MDeQZ+E)GwicgAqGPfL@n`# zK*H|9qrNN31eak}Z15bk%i>?0a?TmhIHBgy&g1|*Q!9#sx_5sJ$&Y2x5n84ZwP)2O zvCV0m`%J7dcQIn1(Y&P!Ambym;HN5_8rY5`<+v3hu`F_}W0ustF~&7g;P-CQ+km|U zAht9hUm*Gj;U?TMjN=zYaO0%C4mCdd*&~9m3=E^Rix-`-8H>*EUqF($Yt$=@-D`!( zo%C6WiDFx%03w~Q0=}>fmuvjTG>T5rP$!Y9ePeg5-ywlEen=u27h6izSU$-o-14{Q zmgn;j1#8@l1e3m7S4CGzp+jTV4&k-c{GuM*DerTwvhBwc20E*SN|BMPGxwOoj;&@$ z+mp{TGkCJu;0UGjTI)J|mSC!_r=+`lK7D>Zu8`nsp%8iAj*zc<{J&grO1+V{8){tJ zFD6a4ZmHvbrS$6h6hdi`%735PatV_l9vWkF_^GI+ZNk6s1qWG|+0l;psszz%^cptGxrgZT>o{qO=2i4yw*0i_4 zgR(>vN_Q;v@8@_>OnD|y^7O*FJho`ueGV*9Sw*?~QM!NA8GkmGF7UI~_K3=u#Hsfq zA}=izxbq4}!fk+spP0ZB3}i=iwNAEe3k+K6`P|wGB9}$`#Jc&piAia93CphCEH2Mf z?)aOyJ`vGyKYjx@-+uFY6w%WpsHtqzvoKx23h8{$K#*S2o0#}FP83_O#penP!Lnsj zWEO9MNx0`pU}LZLX^@B_i+a~0BR!{U*+7epdG%Phh^7W)vFzYiOxKZaWNc1b;3mhZ z(B?*hEIE&P{f(AJ%E2Mdgnp-sTlJWj!j8pW*#2wA8&1;D5R8I(++Hf>;^cTTbPki3 ztxS6RwVg!@_|n#x!HTfa@tg2d*@R3JczX==pC69cUryf!arIa4w#af*5Mw>IYaheH zNE4NF?0szffaiw1PBe+n%22Z#hM8!uOMBi#5lQ=O$d z7`bw@kwX7dx#h6;2N%}8M?7^r03Dc6=|ZqZ=QGF`ilO9im2g)E|7@aQnQ#~BYYPIB z74ZYsSgI_aG912hVzG{rPX7Wg?Bj+FcV7JX_o}Oz+0-;Q zC=Z)pzehv4LP%4SL_0`1QhZX&8ffqm6?LDy^<7*e71WG^Jxx?})k{@y2V|<)WxrH455W9^C|Ki8=#@0Ab)7Fp%eH6 zLe$|%A37}Cr9q}sKaKn*v6dny0`U0EhVs7gX374va@7+?=po?EgA^{Cp-73ztv`Ux zR^i~=y-HTXPfM(ZerK*i$M}Qj}c&CUXrWF=sk|X5M-}CzCg^vPhv0@c&rWq|9 zhnU$#fG=;(dcfW9mYdqsbze>SLmoi#&dVN`h=%w4bCQ|Ae{Yvbr|f_;xWOl#8i`aN z4Zmgr5k&d)SMHnyQ^S6YP3-J%`0lVZQ1R*ovCg)9Jxh{_jj&f<+2+18Bk^31LYvH9 zE@c?T?y(loPtOm={DCH1I=6|eDOaXY#Ke{C%F4xtyrHsCb=O5@$U{ELEWn_z-J&Hh z8j4YU5UC~Ty|z7Eki1uK5Pl{5!_#idUHSQ)@5>U4e!VT0mHS!s=NC`kB`%M-K8 zqVUnla}uf;RqTMk8=nIOK0!$t+FFaIpRShr^+GsdNc@fZaVAo`UK{5GErEGopqfqm zHi>f!1hJVn$!9!h_iB=DgNgmbFat0@zN8YgIJPp-O(y0CrDM*mPv*d*r?E7HKjNPw zH#NFRTUQ9}%^|jdDUAmncL1MjZneEOitHub03YU^H~cd-I4MBMtP2Co*Rui#2tdp* z9Z91f5{xF%ddU3r_?Y)AqbKru#AJZU3J3TVQVfoLEfMTSC1f*&3q`wz(-62I`XK_7 z&+L)`Len_4IG9b*6GStyIRmbGPXvf=9GvX4#Ss_ks27mI>Y8@#O#%E#J^ z2Cg+L>}tSrX$<)52A3=0%Nv-Ex0i(zA};4$Ms{NlaP>LbKy3YeuQfASxG=K=NupxM z$AURQ>{`+Xja#vZ)oT}~hvA9WP`=C!ac)24M%VK&9G~ps#oHy9DZD6Z2kZqGF3R6P zgZZ)Id#`9r4l7o`*+-8APn5pn28tyQ?Esojh%J62=>=Y8UZjE4r#3kCu zR-1}6m=sb*HwgaCrRxD)5eymIA7S5b%KbyA%SN4JJ?eSH<3ghU+a^itpgnlt z)apg0>=_KD{74;^g~<8Qaavcu2e;LdvNUCcm@L{Tm&-0q z9rHYaXi?nVaW$vBc;SU@WYRZ2iwOsGLZz)n-uOKo21?L5OoUdy+>vCQ4rBn^f z!-p(xyuK0Q`!>(u@$ZEJWEj1W^23Q21e*p@BZQ1Zf^I;_%OO{o9+$YQe9aF%e zZZ2bm9UBBQ2;j#Ewf&gKKK_9(QLh0^3WlJ|V8>f|+s5Xn->A-|mEZ{wTA5Dp&AqnU zpY-6)fxi`6k%0s-hNnyYUG)9y{_CD5mtT(p954ggyr1R!qOs+Z@a?#pjt$lmO1RZR zlJhzb+8xC&Y+o1!3$9?%##=WWiM5}*Ht?=|6I8Upi1ee=$szZ&*Y63(Vu|#~ERY4s zr8Uw;1I|0LO+4WR7}y@$`qm#;aQJsj5~mGK=*{(Udae+gUo{%fCpRFp>QWr=Nr`@>AqOXzmBBQQIWnGMM;N8b6}?`X~5 zhg^O=f|9c2o_ryP8?$9%AjeWe1ABx+V=@6eha~am>S@0N5~YC-aaYa&RDueppLN9D^N{G_FVt(_japNo(0I8#_b2@e zg8WU;SZ+j_qY&8yr8Qc-0_Prt-EDIOvcCpnXk)V#jCWd=aV9O_b+n)T^^1U`?bg*< z)hE8T@XcZ6iW5CzD4=p-&#cH$-s;QAu#c`WNI?2h| z`fl>9}?+6#;1kml6Qsvc~H8oM!X20 zl@IJLs{+L7iV%6)ETDLk7$E)FEQDi4l^oKjS*8sq;TNPTad^y(;NbPPRdF`>s-1VR zZ~6-cTj8thPu=i~Lr(8Yt&C*@=2Cs+f-FuNw^=-$MDan@0TZL<5VZ;i3O=@4dLgcx z1CJB9MwpysYvN1;jRl=nuWQ>$x2LH52E%UyzCxxvhTYSy=zaVYsJx6?@A} z47O}ebimNE-qCMPjpQ;yp_3$&TJpFCN&%D(;GdlYx zh*BcIN=WwUSwM(J^)h9)PaotD9#tQQ5ez-XOWib<<7xu_IXX;_CXfP(WSkoWwqXMS zKRWBY($cT?OJrx)0O3R_bJbZ4mHhltuizdXP^;~Dryvh`Lcut3pjSiG9rAb=ngN@5pXEtNU2?y>8`Sago)qZbO@?07lRTUK4zD6>w)^%*iCw*Sty zO;u7(h1_A-R1lL1Y!TOCZ-&QUmuYagHAFi-imJ-rqsOAVh#0WAx#9&Vw2Z&)r_eDB z1v_5)k?&`oOxFBm-Gc`vKD&593+uOZQQeIz7hLOo!j8W07Pfjv5{Zv=x6O%4P^A!s zb(jD2P+iQ6dvd;J>Y8SH_5=i5EYo1T;rh4 z3{$h~e8d`Jj9sR!6!6Uzc|0GuRTYLJGd#nb-m6-~>QA2_KRbB<(HHlmpF&jRlZyZ8 zwGR?^{H{fy#BBPRc|d)hdV<%MC+x^3SCw1Gz=jZ-bprG|1&$JyJQAvIIJ4U5p{ixP z;(&YfmbOvzFl7jxbx5>oUXLoUqs6rgQqRIUhr+iUGDdUA#UX8M9{xyg>D;W8<Bn;{|<0U#Fqq0Xt&S$X=PcIS>$T2QRgv!B* z>UG^d!R{XCysWopG!nehvT$HCKi(=bzpt{uk{CzE`*K7HjxV8Og?Bx8EBJoeDHO6b zi*xyz?Cxi!*JWWBw9$vLrcpUraEuhfiiwGhIN!qVvK9efY@}LP@RKSyYqIsJO-}!G z-T-)>3RKTNd};`=*Xsvhl2rm%_N)<+VX)RQ@&0@LrxqGh-o{Xm!I*F#z z1{-vHbe+Y}*C5?{w#GpNHc}h@ZSP=dNfL5Zp^IqijY^&Wnh>1p*Im&= z#g130c7&Hs2R@I8ib)#+nDGGbWGKL$S*#*s_!HbQX~Uyd9C&3LC~{$ropjk!koSwS zsVj2^qaD@mjpofy#LLXnDc=*xEX7^YdQ3j)EfegVf{WO^7JaYYi^#AKgQ1nd=j!5*te=SD$VIN$9r@Aj5ym+AWbE8=$B5CVqZP4AOWOw`tENFLX$cC$}oUJ15}ICFumP zy<3N1zQM&{o%nv)FVke%$aJCthh#KNZmY`L8211?)%{*wt(sM_|^dy{iz9#zORX#Bd01)g(gHHe9YZ15@>Z z2=QeLz}|{#t07+fsAu>%f?!2-U!-%~$gRe0ve?*IpulF#0CWbMq74NQ=VET%hKmub#*2Lb1cfr#9 zST?9Afm@a)CQZB$rUndTd$SPC8vB6C9#82>U9V+N_(EMjb_G^8!$Ei!GQvd-YvWib z5Hez5A{u?~H^>8HW$9e(bstVybPplMp0E>dsc6@iwZ7bWrau)gk$=q~H66LXUj=9kzvyX?o;P@HtHd zB@0u2B~pGlt|@$+w@H4S0@dO4b>|nM(_6SX7*h<27R`6*L9c)@l?G#3VEjXtO82sd ziHtnwIGa&>{jJTKKHAO+*D0zUwR4FO2-_pg!Tf-eE*;jcnK1->1wOde zDs0*o;}=92N4zqJcJAeYBo%%oE6MS2EomwC9n$DY@No;HNdM&g{0|FwEQUiv%*fUd zt1HqM{@!5VM(;sNEJSGYWy!GNiCre|PthP-?*V~2{z0^9bIz2B1@>#qe$T>K zTA(V@k~}cd3%8rY>geQn6^yAB>a<14Jv4k{+UHOUx#;fV!T=p*k^Ct+1zecs10FNA zFerW-ILrGbl!P53ODphujnr1fD#hywl!0IdMf!tNi7lKkOq zv=Iw7)jMpCt3ZdrymPr~lK|M7g>o(*5U|S^U;&DcoS>Lp2!BQS@K7(ntO?s0B4NC8 z0p!8|vpoMBatsiaX@Fce= zHhkvDDA0v3)|3@1bFj1f2|45=X1ZZ-1l=l-l2%5_EVdJrX8&`CdoaGZ6y6x zn*V7x85a{1^9Dr#fAQxRRSvv?7LiVvq;~An0AbF^mG)2HOuCu{&Z2^yBRDfQpIOYp z#`RB{+E*grjD#n8m8!qv7NuY8&POB*;SuAsD&`b^zFHAInT;8`)ozSl=|uLZ6)a}x z$Q!#HjD5KZ`o&*Xb6{eMy1>El^SJM;#R^}|RsHFz>^BvUe77=Q4-W@z_}v6kHN zZ8!Gra8#hU5b50Pc(|1#F~HiFqXkDJSC{`Z(IA?o@r%Rj6$!JX1Xj6mboFmcRs{hyZ=?fKaDmR47Jy|c9rI`?{U(|>N@L;x literal 0 HcmV?d00001 diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee index b1a464829c..1e3b6b0ebe 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -4,6 +4,7 @@ should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" SandboxedModule = require('sandboxed-module') +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "AuthorizationManager", -> beforeEach -> @@ -11,6 +12,7 @@ describe "AuthorizationManager", -> "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} "../../models/Project": Project: @Project = {} "../../models/User": User: @User = {} + "../Errors/Errors": Errors @user_id = "user-id-1" @project_id = "project-id-1" @callback = sinon.stub() @@ -91,6 +93,16 @@ describe "AuthorizationManager", -> it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "when the project doesn't exist", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, null) + + it "should return a NotFoundError", -> + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, (error) -> + error.should.be.instanceof Errors.NotFoundError describe "canUserReadProject", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee index 7e1ca2d5ef..bc62e603de 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -4,16 +4,21 @@ should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddlewear.js" SandboxedModule = require('sandboxed-module') +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "AuthorizationMiddlewear", -> beforeEach -> @AuthorizationMiddlewear = SandboxedModule.require modulePath, requires: "./AuthorizationManager": @AuthorizationManager = {} "logger-sharelatex": {log: () ->} + "mongojs": ObjectId: @ObjectId = {} + "../Errors/Errors": Errors @user_id = "user-id-123" @project_id = "project-id-123" @req = {} @res = {} + @ObjectId.isValid = sinon.stub() + @ObjectId.isValid.withArgs(@project_id).returns true @next = sinon.stub() METHODS_TO_TEST = { @@ -90,6 +95,17 @@ describe "AuthorizationMiddlewear", -> @AuthorizationMiddlewear.redirectToRestricted .calledWith(@req, @res, @next) .should.equal true + + describe "with malformed project id", -> + beforeEach -> + @req.params = + project_id: "blah" + @ObjectId.isValid = sinon.stub().returns false + + it "should return a not found error", (done) -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, (error) -> + error.should.be.instanceof Errors.NotFoundError + done() describe "ensureUserIsSiteAdmin", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f659cfebe6..632b43e7f2 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -5,6 +5,7 @@ path = require('path') sinon = require('sinon') modulePath = path.join __dirname, "../../../../app/js/Features/Collaborators/CollaboratorsHandler" expect = require("chai").expect +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "CollaboratorsHandler", -> beforeEach -> @@ -16,6 +17,7 @@ describe "CollaboratorsHandler", -> "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} + "../Errors/Errors": Errors @project_id = "mock-project-id" @user_id = "mock-user-id" @@ -24,25 +26,35 @@ describe "CollaboratorsHandler", -> @callback = sinon.stub() describe "getMemberIdsWithPrivilegeLevels", -> - beforeEach -> - @Project.findOne = sinon.stub() - @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { - owner_ref: [ "owner-ref" ] - readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] - collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] - }) - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback + describe "with project", -> + beforeEach -> + @Project.findOne = sinon.stub() + @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + owner_ref: [ "owner-ref" ] + readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] + collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] + }) + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback - it "should return an array of member ids with their privilege levels", -> - @callback - .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "owner" } - { id: "read-only-ref-1", privilegeLevel: "readOnly" } - { id: "read-only-ref-2", privilegeLevel: "readOnly" } - { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } - { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } - ]) - .should.equal true + it "should return an array of member ids with their privilege levels", -> + @callback + .calledWith(null, [ + { id: "owner-ref", privilegeLevel: "owner" } + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "with a missing project", -> + beforeEach -> + @Project.findOne = sinon.stub().yields(null, null) + + it "should return a NotFoundError", (done) -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) -> + error.should.be.instanceof Errors.NotFoundError + done() describe "getMemberIds", -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 0cacffb5cf..5d54151483 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,83 +1,8 @@ -request = require("request") expect = require("chai").expect -async = require "async" -settings = require("settings-sharelatex") -{db} = require("../../../app/js/infrastructure/mongojs") - -count = 0 -BASE_URL = "http://localhost:3000" - -request = request.defaults({ - baseUrl: BASE_URL, - followRedirect: false -}) - -class User - constructor: (options = {}) -> - @email = "acceptance-test-#{count}@example.com" - @password = "acceptance-test-#{count}-password" - count++ - @jar = request.jar() - @request = request.defaults({ - jar: @jar - }) - - login: (callback = (error) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/register" # Register will log in, but also ensure user exists - json: - email: @email - password: @password - }, (error, response, body) => - return callback(error) if error? - db.users.findOne {email: @email}, (error, user) => - return callback(error) if error? - @id = user?._id?.toString() - callback() - - createProject: (name, callback = (error, project_id) ->) -> - @request.post { - url: "/project/new", - json: - projectName: name - }, (error, response, body) -> - return callback(error) if error? - if !body?.project_id? - console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body - callback(null, body.project_id) - - addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> - @request.post { - url: "/project/#{project_id}/users", - json: {email, privileges} - }, (error, response, body) -> - return callback(error) if error? - callback(null, body.user) - - makePublic: (project_id, level, callback = (error) ->) -> - @request.post { - url: "/project/#{project_id}/settings/admin", - json: - publicAccessLevel: level - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - getCsrfToken: (callback = (error) ->) -> - @request.get { - url: "/register" - }, (err, response, body) => - return callback(error) if error? - csrfMatches = body.match("window.csrfToken = \"(.*?)\";") - if !csrfMatches? - return callback(new Error("no csrf token found")) - @request = @request.defaults({ - headers: - "x-csrf-token": csrfMatches[1] - }) - callback() +async = require("async") +User = require "./helpers/User" +request = require "./helpers/request" +settings = require "settings-sharelatex" try_read_access = (user, project_id, test, callback) -> async.series [ diff --git a/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee b/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee new file mode 100644 index 0000000000..19e5f445f7 --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee @@ -0,0 +1,21 @@ +expect = require("chai").expect +async = require("async") +User = require "./helpers/User" + +describe "Project CRUD", -> + before (done) -> + @user = new User() + @user.login done + + describe "when project doesn't exist", -> + it "should return 404", (done) -> + @user.request.get "/project/aaaaaaaaaaaaaaaaaaaaaaaa", (err, res, body) -> + expect(res.statusCode).to.equal 404 + done() + + describe "when project has malformed id", -> + it "should return 404", (done) -> + @user.request.get "/project/blah", (err, res, body) -> + expect(res.statusCode).to.equal 404 + done() + \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee new file mode 100644 index 0000000000..f1c38029d7 --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -0,0 +1,74 @@ +request = require("./request") +settings = require("settings-sharelatex") +{db} = require("../../../../app/js/infrastructure/mongojs") + +count = 0 + +class User + constructor: (options = {}) -> + @email = "acceptance-test-#{count}@example.com" + @password = "acceptance-test-#{count}-password" + count++ + @jar = request.jar() + @request = request.defaults({ + jar: @jar + }) + + login: (callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/register" # Register will log in, but also ensure user exists + json: + email: @email + password: @password + }, (error, response, body) => + return callback(error) if error? + db.users.findOne {email: @email}, (error, user) => + return callback(error) if error? + @id = user?._id?.toString() + callback() + + createProject: (name, callback = (error, project_id) ->) -> + @request.post { + url: "/project/new", + json: + projectName: name + }, (error, response, body) -> + return callback(error) if error? + if !body?.project_id? + console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body + callback(null, body.project_id) + + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> + @request.post { + url: "/project/#{project_id}/users", + json: {email, privileges} + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.user) + + makePublic: (project_id, level, callback = (error) ->) -> + @request.post { + url: "/project/#{project_id}/settings/admin", + json: + publicAccessLevel: level + }, (error, response, body) -> + return callback(error) if error? + callback(null) + + getCsrfToken: (callback = (error) ->) -> + @request.get { + url: "/register" + }, (err, response, body) => + return callback(error) if error? + csrfMatches = body.match("window.csrfToken = \"(.*?)\";") + if !csrfMatches? + return callback(new Error("no csrf token found")) + @request = @request.defaults({ + headers: + "x-csrf-token": csrfMatches[1] + }) + callback() + +module.exports = User \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/helpers/request.coffee b/services/web/test/acceptance/coffee/helpers/request.coffee new file mode 100644 index 0000000000..879acd843a --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/request.coffee @@ -0,0 +1,5 @@ +BASE_URL = "http://localhost:3000" +module.exports = require("request").defaults({ + baseUrl: BASE_URL, + followRedirect: false +}) \ No newline at end of file From 2a9e451876c69b56ef9c19970dc68b5105f6f28a Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 11:55:59 +0000 Subject: [PATCH 38/48] Refine error page to in all situations, and don't send sentry errors on not found errors --- .../Features/Errors/ErrorController.coffee | 3 +- services/web/app/coffee/router.coffee | 6 +-- services/web/app/views/general/500.jade | 37 +++++++++++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index a0ce6c85c9..255c747bea 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -17,8 +17,9 @@ module.exports = ErrorController = logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" res.sendStatus(403) return - logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" if error instanceof Errors.NotFoundError + logger.warn {err: error, url: req.url}, "not found error" ErrorController.notFound req, res else + logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" ErrorController.serverError req, res \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 305b3bd564..60f1d6b7c4 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -221,9 +221,9 @@ module.exports = class Router headers: req.headers }) - apiRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) - apiRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") - apiRouter.get '/oops-mongo', (req, res, next) -> + webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) + webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") + webRouter.get '/oops-mongo', (req, res, next) -> require("./models/Project").Project.findOne {}, () -> throw new Error("Test error") diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade index 12983b923b..e045a7d457 100644 --- a/services/web/app/views/general/500.jade +++ b/services/web/app/views/general/500.jade @@ -1,16 +1,21 @@ -extends ../layout - -block content - .content - .container - .row - .col-md-8.col-md-offset-2.text-center - .page-header - h2 Oh dear, something went wrong. - p: img(src="/img/lion-sad-128.png", alt="Sad Lion") - p - | Something went wrong with your request, sorry. Our staff are probably looking into this, but it continues, please contact us at #{settings.adminEmail} - p - a(href="/") - i.fa.fa-arrow-circle-o-left - | #{translate("take_me_home")} +doctype html +html(itemscope, itemtype='http://schema.org/Product') + head + title Something went wrong + link(rel="icon", href="/favicon.ico") + link(rel='stylesheet', href='/stylesheets/style.css') + link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") + body + .content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h2 Oh dear, something went wrong. + p: img(src="/img/lion-sad-128.png", alt="Sad Lion") + p + | Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail} + p + a(href="/") + i.fa.fa-arrow-circle-o-left + | Take me home From 0ba70e7ccc83670215dd44141989c10b4043bf0c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:15:57 +0000 Subject: [PATCH 39/48] Remove missing parameter in log lines --- .../web/app/coffee/Features/Uploads/ArchiveManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index a615810fe6..41d041109d 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -25,7 +25,7 @@ module.exports = ArchiveManager = error += chunk unzip.on "error", (err) -> - logger.error {err, source, destination}, "unzip failed" + logger.error {err, source}, "unzip failed" if err.code == "ENOENT" logger.error "unzip command not found. Please check the unzip command is installed" callback(err) @@ -33,7 +33,7 @@ module.exports = ArchiveManager = unzip.on "exit", () -> if error? error = new Error(error) - logger.error err:error, source: source, destination: destination, "error checking zip size" + logger.error err:error, source: source, "error checking zip size" lines = output.split("\n") lastLine = lines[lines.length - 2]?.trim() From 8fb3e629e86e306a531d494f463f0f6d6bd861bd Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:23:14 +0000 Subject: [PATCH 40/48] Require logins for all uploads to projects --- services/web/app/coffee/Features/Uploads/UploadsRouter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index fdff8d0ea3..d87d271a7f 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -16,6 +16,7 @@ module.exports = maxRequests: 200 timeInterval: 60 * 30 }), + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanWriteProjectContent, ProjectUploadController.uploadFile From 6beb29f4498b9885357f016e56d571756b84ec7c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:28:53 +0000 Subject: [PATCH 41/48] Don't treat no root resource as a fatal error --- .../Features/Compile/ClsiManager.coffee | 26 +++++++++---------- .../coffee/Compile/ClsiManagerTests.coffee | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 8b7ef023bd..174ebe830b 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -86,6 +86,9 @@ module.exports = ClsiManager = rootResourcePathOverride = path rootResourcePath = rootResourcePathOverride if rootResourcePathOverride? + if !rootResourcePath? + logger.warn {project_id}, "no root document found, setting to main.tex" + rootResourcePath = "main.tex" for path, file of files path = path.replace(/^\//, "") # Remove leading / @@ -94,19 +97,16 @@ module.exports = ClsiManager = url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}" modified: file.created?.getTime() - if !rootResourcePath? - callback new Error("no root document exists") - else - callback null, { - compile: - options: - compiler: project.compiler - timeout: options.timeout - imageName: project.imageName - draft: !!options.draft - rootResourcePath: rootResourcePath - resources: resources - } + callback null, { + compile: + options: + compiler: project.compiler + timeout: options.timeout + imageName: project.imageName + draft: !!options.draft + rootResourcePath: rootResourcePath + resources: resources + } wordCount: (project_id, file, options, callback = (error, response) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index baf2643c05..3aa5b80528 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -19,7 +19,7 @@ describe "ClsiManager", -> url: "https://clsipremium.example.com" "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } "request": @request = {} @project_id = "project-id" @callback = sinon.stub() @@ -218,9 +218,9 @@ describe "ClsiManager", -> @project.rootDoc_id = "not-valid" @ClsiManager._buildRequest @project, null, (@error, @request) => done() - - it "should return an error", -> - expect(@error).to.exist + + it "should set to main.tex", -> + @request.compile.rootResourcePath.should.equal "main.tex" describe "with the draft option", -> it "should add the draft option into the request", (done) -> From 8bfc613bb3d8e908bae8127d293278cc968b34af Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:29:34 +0000 Subject: [PATCH 42/48] Log client side errors as warns so they don't show in Sentry --- services/web/app/coffee/router.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 60f1d6b7c4..5e16073ed3 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -232,7 +232,7 @@ module.exports = class Router res.send() webRouter.post '/error/client', (req, res, next) -> - logger.error err: req.body.error, meta: req.body.meta, "client side error" + logger.warn err: req.body.error, meta: req.body.meta, "client side error" res.sendStatus(204) webRouter.get '*', ErrorController.notFound \ No newline at end of file From 9a0ec9c292101fc02b1cad44fc882d57fa39e9ef Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:54:45 +0000 Subject: [PATCH 43/48] Don't throw fatal error when recently compiled --- .../coffee/Features/Compile/CompileManager.coffee | 3 ++- .../coffee/Compile/CompileManagerTests.coffee | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 7cac827789..c89e7107dd 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -26,7 +26,8 @@ module.exports = CompileManager = CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) -> return callback(error) if error? if recentlyCompiled - return callback new Error("project was recently compiled so not continuing") + logger.warn {project_id, user_id}, "project was recently compiled so not continuing" + return callback null, "too-recently-compiled", [] CompileManager._ensureRootDocumentIsSet project_id, (error) -> return callback(error) if error? diff --git a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee index e68f5fa422..814b4f258b 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee @@ -27,7 +27,7 @@ describe "CompileManager", -> Timer: class Timer done: sinon.stub() inc: sinon.stub() - "logger-sharelatex": @logger = { log: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub() } @project_id = "mock-project-id-123" @user_id = "mock-user-id-123" @callback = sinon.stub() @@ -90,15 +90,12 @@ describe "CompileManager", -> .should.equal true describe "when the project has been recently compiled", -> - beforeEach -> + it "should return", (done)-> @CompileManager._checkIfAutoCompileLimitHasBeenHit = (_, cb)-> cb(null, true) @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, true) - @CompileManager.compile @project_id, @user_id, {}, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("project was recently compiled so not continuing")) - .should.equal true + @CompileManager.compile @project_id, @user_id, {}, (err, status)-> + status.should.equal "too-recently-compiled" + done() describe "should check the rate limit", -> it "should return", (done)-> From 4d7ed1cb17cc7f587c45eedf9a91d8eb62e6302a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 21 Mar 2016 16:00:12 +0000 Subject: [PATCH 44/48] improved logging from unzip command --- .../web/app/coffee/Features/Uploads/ArchiveManager.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index 41d041109d..5185c5d49c 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -39,11 +39,11 @@ module.exports = ArchiveManager = lastLine = lines[lines.length - 2]?.trim() totalSizeInBytes = lastLine?.split(" ")?[0] - totalSizeInBytes = parseInt(totalSizeInBytes) + totalSizeInBytesAsInt = parseInt(totalSizeInBytes) - if !totalSizeInBytes? or isNaN(totalSizeInBytes) - logger.err source:source, "error getting bytes of zip" - return callback(new Error("something went wrong")) + if !totalSizeInBytesAsInt? or isNaN(totalSizeInBytesAsInt) + logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, "error getting bytes of zip" + return callback(new Error("error getting bytes of zip")) isTooLarge = totalSizeInBytes > (ONE_MEG * 300) From 840d3b75bb4cd8f93dc2435362653be8c332a2e1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 21 Mar 2016 16:37:29 +0000 Subject: [PATCH 45/48] listen for close not exit when working with unzip command sometimes the command will fail because stout has not finished yet --- .../coffee/Features/Uploads/ArchiveManager.coffee | 8 +++----- .../coffee/Uploads/ArchiveManagerTests.coffee | 14 +++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index 5185c5d49c..645828ca6d 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -30,7 +30,7 @@ module.exports = ArchiveManager = logger.error "unzip command not found. Please check the unzip command is installed" callback(err) - unzip.on "exit", () -> + unzip.on "close", (exitCode) -> if error? error = new Error(error) logger.error err:error, source: source, "error checking zip size" @@ -42,7 +42,7 @@ module.exports = ArchiveManager = totalSizeInBytesAsInt = parseInt(totalSizeInBytes) if !totalSizeInBytesAsInt? or isNaN(totalSizeInBytesAsInt) - logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, "error getting bytes of zip" + logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, exitCode:exitCode, "error getting bytes of zip" return callback(new Error("error getting bytes of zip")) isTooLarge = totalSizeInBytes > (ONE_MEG * 300) @@ -50,8 +50,6 @@ module.exports = ArchiveManager = callback(error, isTooLarge) - - extractZipArchive: (source, destination, _callback = (err) ->) -> callback = (args...) -> @@ -87,7 +85,7 @@ module.exports = ArchiveManager = logger.error "unzip command not found. Please check the unzip command is installed" callback(err) - unzip.on "exit", () -> + unzip.on "close", () -> timer.done() if error? error = new Error(error) diff --git a/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee b/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee index 89b14c78c9..eab3d2fcbd 100644 --- a/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee @@ -39,7 +39,7 @@ describe "ArchiveManager", -> describe "successfully", -> beforeEach (done) -> @ArchiveManager.extractZipArchive @source, @destination, done - @process.emit "exit" + @process.emit "close" it "should run unzip", -> @child.spawn.calledWithExactly("unzip", [@source, "-d", @destination]).should.equal true @@ -56,7 +56,7 @@ describe "ArchiveManager", -> @callback(error) done() @process.stderr.emit "data", "Something went wrong" - @process.emit "exit" + @process.emit "close" it "should return the callback with an error", -> @callback.calledWithExactly(new Error("Something went wrong")).should.equal true @@ -99,35 +99,35 @@ describe "ArchiveManager", -> isTooLarge.should.equal false done() @process.stdout.emit "data", @output("109042") - @process.emit "exit" + @process.emit "close" it "should return true with large bytes", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => isTooLarge.should.equal true done() @process.stdout.emit "data", @output("1090000000000000042") - @process.emit "exit" + @process.emit "close" it "should return error on no data", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", "" - @process.emit "exit" + @process.emit "close" it "should return error if it didn't get a number", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", @output("total_size_string") - @process.emit "exit" + @process.emit "close" it "should return error if the is only a bit of data", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", " Length Date Time Name \n--------" - @process.emit "exit" + @process.emit "close" describe "findTopLevelDirectory", -> beforeEach -> From 77918059493fa0182efc89b4e561ac15c93a1132 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 17:03:31 +0000 Subject: [PATCH 46/48] Allow admin access to projects --- .../Authorization/AuthorizationManager.coffee | 7 +++- .../AuthorizationManagerTests.coffee | 33 +++++++++++++++++++ .../coffee/AuthorizationTests.coffee | 29 +++++++++++++++- .../acceptance/coffee/helpers/User.coffee | 5 ++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index eb6f564fea..ded0b6f979 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -33,7 +33,12 @@ module.exports = AuthorizationManager = # The user has direct access callback null, privilegeLevel, false else - getPublicAccessLevel() + AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> + return callback(error) if error? + if isAdmin + callback null, PrivilegeLevels.OWNER, false + else + getPublicAccessLevel() canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee index 1e3b6b0ebe..fcacce5164 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -20,6 +20,7 @@ describe "AuthorizationManager", -> describe "getPrivilegeLevelForProject", -> beforeEach -> @Project.findOne = sinon.stub() + @AuthorizationManager.isUserSiteAdmin = sinon.stub() @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() describe "with a private project", -> @@ -30,6 +31,7 @@ describe "AuthorizationManager", -> describe "with a user_id with a privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, "readOnly") @@ -40,6 +42,7 @@ describe "AuthorizationManager", -> describe "with a user_id with no privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, false) @@ -48,6 +51,17 @@ describe "AuthorizationManager", -> it "should return false", -> @callback.calledWith(null, false, false).should.equal true + describe "with a user_id who is an admin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user as an owner", -> + @callback.calledWith(null, "owner", false).should.equal true + describe "with no user (anonymous)", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback @@ -55,6 +69,9 @@ describe "AuthorizationManager", -> it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + it "should not call AuthorizationManager.isUserSiteAdmin", -> + @AuthorizationManager.isUserSiteAdmin.called.should.equal false + it "should return false", -> @callback.calledWith(null, false, false).should.equal true @@ -66,6 +83,7 @@ describe "AuthorizationManager", -> describe "with a user_id with a privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, "readOnly") @@ -76,6 +94,7 @@ describe "AuthorizationManager", -> describe "with a user_id with no privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, false) @@ -84,6 +103,17 @@ describe "AuthorizationManager", -> it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true + describe "with a user_id who is an admin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user as an owner", -> + @callback.calledWith(null, "owner", false).should.equal true + describe "with no user (anonymous)", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback @@ -91,6 +121,9 @@ describe "AuthorizationManager", -> it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + it "should not call AuthorizationManager.isUserSiteAdmin", -> + @AuthorizationManager.isUserSiteAdmin.called.should.equal false + it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 5d54151483..177750c28d 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -134,11 +134,16 @@ describe "Authorization", -> @other1 = new User() @other2 = new User() @anon = new User() - async.series [ + @site_admin = new User({email: "admin@example.com"}) + async.parallel [ (cb) => @owner.login cb (cb) => @other1.login cb (cb) => @other2.login cb (cb) => @anon.getCsrfToken cb + (cb) => + @site_admin.login (err) => + return cb(err) if error? + @site_admin.ensure_admin cb ], done describe "private project", -> @@ -151,6 +156,9 @@ describe "Authorization", -> it "should allow the owner read access to it", (done) -> expect_read_access @owner, @project_id, done + it "should allow the owner write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done + it "should allow the owner write access to its settings", (done) -> expect_settings_write_access @owner, @project_id, done @@ -160,6 +168,9 @@ describe "Authorization", -> it "should not allow another user read access to the project", (done) -> expect_no_read_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow another user write access to its content", (done) -> + expect_no_content_write_access @other1, @project_id, done + it "should not allow another user write access to its settings", (done) -> expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done @@ -169,11 +180,27 @@ describe "Authorization", -> it "should not allow anonymous user read access to it", (done) -> expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done + it "should not allow anonymous user write access to its content", (done) -> + expect_no_content_write_access @anon, @project_id, done + it "should not allow anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user admin access to it", (done) -> expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done + + it "should allow site admin users read access to it", (done) -> + expect_read_access @site_admin, @project_id, done + + it "should allow site admin users write access to its content", (done) -> + expect_content_write_access @site_admin, @project_id, done + + it "should allow site admin users write access to its settings", (done) -> + expect_settings_write_access @site_admin, @project_id, done + + it "should allow site admin users admin access to it", (done) -> + expect_admin_access @site_admin, @project_id, done + describe "shared project", -> before (done) -> diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index f1c38029d7..c13a45499d 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -1,6 +1,6 @@ request = require("./request") settings = require("settings-sharelatex") -{db} = require("../../../../app/js/infrastructure/mongojs") +{db, ObjectId} = require("../../../../app/js/infrastructure/mongojs") count = 0 @@ -29,6 +29,9 @@ class User @id = user?._id?.toString() callback() + ensure_admin: (callback = (error) ->) -> + db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback + createProject: (name, callback = (error, project_id) ->) -> @request.post { url: "/project/new", From 2af2dd694e659d27797b01e6843ef5f962acd4fe Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 09:39:25 +0000 Subject: [PATCH 47/48] Use null to represent anonymous user, as AuthorizationManager expects --- .../app/coffee/Features/Project/ProjectController.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index dc64f46bff..34a44994be 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -188,7 +188,7 @@ module.exports = ProjectController = anonymous = false else anonymous = true - user_id = 'openUser' + user_id = null project_id = req.params.Project_id logger.log project_id:project_id, "loading editor" @@ -197,14 +197,14 @@ module.exports = ProjectController = project: (cb)-> ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb user: (cb)-> - if user_id == 'openUser' + if !user_id? cb null, defaultSettingsForAnonymousUser(user_id) else User.findById user_id, (err, user)-> logger.log project_id:project_id, user_id:user_id, "got user" cb err, user subscription: (cb)-> - if user_id == 'openUser' + if !user_id? return cb() SubscriptionLocator.getUsersSubscription user_id, cb activate: (cb)-> From f182fbf3963ebfd3a539edda52f1ecc52a1615fe Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 09:53:47 +0000 Subject: [PATCH 48/48] Convert 'anonymous-user' from real-time api in 'null' internally --- .../Features/Editor/EditorHttpController.coffee | 2 ++ .../coffee/Editor/EditorHttpControllerTests.coffee | 11 +++++++++++ .../test/acceptance/coffee/AuthorizationTests.coffee | 6 +++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index ca4d70b1aa..379f20fe5b 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -15,6 +15,8 @@ module.exports = EditorHttpController = joinProject: (req, res, next) -> project_id = req.params.Project_id user_id = req.query.user_id + if user_id == "anonymous-user" + user_id = null logger.log {user_id, project_id}, "join project request" Metrics.inc "editor.join-project" EditorHttpController._buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) -> diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 6a594f31ee..a98c34af28 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -77,6 +77,17 @@ describe "EditorHttpController", -> @ProjectDeleter.unmarkAsDeletedByExternalSource .calledWith(@project_id) .should.equal true + + describe "with an anonymous user", -> + beforeEach -> + @req.query = + user_id: "anonymous-user" + @EditorHttpController.joinProject @req, @res + + it "should pass the user id as null", -> + @EditorHttpController._buildJoinProjectView + .calledWith(@project_id, null) + .should.equal true describe "_buildJoinProjectView", -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 177750c28d..c6678656b9 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -56,9 +56,13 @@ try_admin_access = (user, project_id, test, callback) -> try_content_access = (user, project_id, test, callback) -> # The real-time service calls this end point to determine the user's # permissions. + if user.id? + user_id = user.id + else + user_id = "anonymous-user" request.post { url: "/project/#{project_id}/join" - qs: {user_id: user.id} + qs: {user_id} auth: user: settings.apis.web.user pass: settings.apis.web.pass