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..428e4d506d 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,12 @@ 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: + timeout: 10000 + reporter: grunt.option('reporter') or 'spec' + grep: grunt.option("grep") "git-rev-parse": version: @@ -184,6 +199,7 @@ module.exports = (grunt) -> ] "Test tasks": [ "test:unit" + "test:acceptance" ] "Run tasks": [ "run" @@ -290,6 +306,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 +314,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/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/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 d815b426fe..201643e88e 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -35,19 +35,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/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee new file mode 100644 index 0000000000..ded0b6f979 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -0,0 +1,73 @@ +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +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 + # 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? + return callback new Errors.NotFoundError("no project found with id #{project_id}") + if project.publicAccesLevel == PublicAccessLevels.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 + 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 != PrivilegeLevels.NONE + # The user has direct access + callback null, privilegeLevel, false + else + 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) -> + return callback(error) if error? + 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 [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 == PrivilegeLevels.OWNER + return callback null, true + else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE 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 == PrivilegeLevels.OWNER) + + isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> + 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 new file mode 100644 index 0000000000..4888db0c8a --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -0,0 +1,111 @@ +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) -> + 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) -> + 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) -> + 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) -> + 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) -> + 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) -> + 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")) + 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) + + _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? + 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/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/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 788b782eb2..1905d0d0a6 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -7,15 +7,6 @@ UserGetter = require "../User/UserGetter" 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) -> - return next(error) if error? - ProjectGetter.populateProjectWithUsers project, (error, project) -> - return next(error) if error? - CollaboratorsController._formatCollaborators project, (error, collaborators) -> - return next(error) if error? - res.send(JSON.stringify(collaborators)) - addUserToProject: (req, res, next) -> project_id = req.params.Project_id LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => @@ -59,29 +50,3 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - _formatCollaborators: (project, 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 - } - - 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 - pushCollaborator(user, ["read", "write"], false) - - if project.readOnly_refs? and project.readOnly_refs.length > 0 - for user in project.readOnly_refs - 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 4ab855af80..71737eecff 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -1,13 +1,77 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project -ProjectEntityHandler = require("../Project/ProjectEntityHandler") mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" 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 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 [] + 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: PrivilegeLevels.READ_AND_WRITE } + 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? + async.mapLimit members, 3, + (member, cb) -> + UserGetter.getUser member.id, (error, user) -> + return cb(error) if error? + return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) + 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 + if member.id == user_id?.toString() + return callback null, member.privilegeLevel + return callback null, PrivilegeLevels.NONE + + getMemberCount: (project_id, callback = (error, count) ->) -> + 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 = []) -> + return callback(error) if error? + for member in members + if member.id.toString() == user_id.toString() + return callback null, true, member.privilegeLevel + return callback null, false, null + + 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)=> + 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" conditions = _id:project_id @@ -38,10 +102,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 @@ -57,6 +121,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/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index d5abf23350..34a6da9a02 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -1,11 +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 - 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 + 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/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/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/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index c52d0f5c67..379f20fe5b 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -5,14 +5,18 @@ 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") +PrivilegeLevels = require "../Authorization/PrivilegeLevels" 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) -> @@ -29,17 +33,17 @@ 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? - AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !canAccess + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE callback null, null, false else callback(null, - ProjectEditorHandler.buildProjectModelView(project), + ProjectEditorHandler.buildProjectModelView(project, members), privilegeLevel ) 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/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index d0589ba5ed..255c747bea 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -1,5 +1,25 @@ +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 + 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/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/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 9a04fafd15..34a44994be 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") @@ -13,10 +12,12 @@ 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") +ProjectGetter = require("./ProjectGetter") +PrivilegeLevels = require("../Authorization/PrivilegeLevels") module.exports = ProjectController = @@ -41,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 @@ -129,7 +138,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) -> @@ -179,23 +188,23 @@ 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" async.parallel { project: (cb)-> - Project.findPopulatedById project_id, 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)-> @@ -216,8 +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" - SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)-> - if !canAccess + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> + return next(error) if error? + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? @@ -228,7 +238,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/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index cdbc933135..8ba8a65845 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -5,6 +5,7 @@ documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') tagsHandler = require("../Tags/TagsHandler") async = require("async") FileStoreHandler = require("../FileStore/FileStoreHandler") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = ProjectDeleter = @@ -44,16 +45,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/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index 3b3233157d..7bd1d33561 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 diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index f12a7d548e..a3d8319424 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,27 @@ module.exports = ProjectEditorHandler = spellCheckLanguage: project.spellCheckLanguage deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs + members: [] + + owner = null + for member in members + if member.privilegeLevel == "owner" + owner = member.user + else + result.members.push @buildUserModelView member.user, member.privilegeLevel + if owner? + result.owner = @buildUserModelView owner, "owner" - if options.includeUsers - result.features = - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false + result.features = _.defaults(owner?.features or {}, { + collaborators: -1 # Infinite + versioning: false + dropbox:false + compileTimeout: 60 + compileGroup:"standard" + templates: false + references: false + }) - 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 - - - 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 d8bb1e457a..ff95c02c40 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -2,11 +2,10 @@ mongojs = require("../../infrastructure/mongojs") db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" +Project = require("../../models/Project").Project Errors = require("../../errors") logger = require("logger-sharelatex") - - module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 @@ -51,42 +50,11 @@ module.exports = ProjectGetter = return callback(err) callback(null, project?[0]) - 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) -> + + 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.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> 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 + callback null, projects, readAndWriteProjects, readOnlyProjects diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index 7d1eb8f749..cd4adad142 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -4,6 +4,7 @@ Errors = require "../../errors" _ = require('underscore') logger = require('logger-sharelatex') async = require('async') +ProjectGetter = require "./ProjectGetter" module.exports = ProjectLocator = findElement: (options, _callback = (err, element, path, parentFolder)->)-> @@ -131,7 +132,8 @@ module.exports = ProjectLocator = 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/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 0b2ddb1e26..bd31345f87 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -1,7 +1,8 @@ logger = require("logger-sharelatex") request = require("request") settings = require("settings-sharelatex") -Project = require("../../models/Project").Project +ProjectGetter = require "../Project/ProjectGetter" +UserGetter = require "../User/UserGetter" 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) + 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.findPopulatedById 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) @@ -44,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.findPopulatedById 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) 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/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/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index e2e021bd97..fb378e45f6 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") SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder') @@ -31,7 +31,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)-> @@ -80,7 +80,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) @@ -109,7 +109,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" @@ -117,7 +117,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 @@ -138,7 +138,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 @@ -150,14 +150,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)-> @@ -166,7 +166,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" @@ -176,7 +176,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)-> @@ -195,7 +195,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 @@ -212,7 +212,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" @@ -225,7 +225,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/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 521aabeb0c..2f3b7fb9a8 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) @@ -122,9 +123,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/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index a615810fe6..645828ca6d 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -25,33 +25,31 @@ 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) - unzip.on "exit", () -> + unzip.on "close", (exitCode) -> 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() 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, exitCode:exitCode, "error getting bytes of zip" + return callback(new Error("error getting bytes of zip")) isTooLarge = totalSizeInBytes > (ONE_MEG * 300) 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/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index c82144acc0..d87d271a7f 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,7 @@ module.exports = maxRequests: 200 timeInterval: 60 * 30 }), - SecurityManager.requestCanModifyProject, + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanWriteProjectContent, ProjectUploadController.uploadFile 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/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/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/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index dc9f0927fd..6397025da5 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -43,33 +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]) - -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) - - - - - applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (folder, fun)-> _.each folder.fileRefs, (file)-> fun(file) diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee new file mode 100644 index 0000000000..3349dafa9b --- /dev/null +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -0,0 +1,23 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +ProjectInviteSchema = new Schema + project_id: ObjectId + from_user_id: ObjectId + privilegeLevel: String + # For existing users + to_user_id: ObjectId + # For non-existant users + hashed_token: String + email: String + +conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) + +ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) + +mongoose.model 'ProjectInvite', ProjectInviteSchema +exports.ProjectInvite = ProjectInvite +exports.ProjectInviteSchema = ProjectInviteSchema \ No newline at end of file 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) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 46b1db2157..5e16073ed3 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 @@ -88,8 +87,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 @@ -100,12 +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 + }), 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 @@ -222,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") @@ -233,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 diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade new file mode 100644 index 0000000000..e045a7d457 --- /dev/null +++ b/services/web/app/views/general/500.jade @@ -0,0 +1,21 @@ +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 diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 3f73439cd0..0b3ee90e96 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -259,7 +259,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"] == 'true' 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/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/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/public/img/lion-sad-128.png b/services/web/public/img/lion-sad-128.png new file mode 100644 index 0000000000..e5e58c42b0 Binary files /dev/null and b/services/web/public/img/lion-sad-128.png differ 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/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee new file mode 100644 index 0000000000..fcacce5164 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -0,0 +1,385 @@ +sinon = require('sinon') +chai = require('chai') +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 -> + @AuthorizationManager = SandboxedModule.require modulePath, requires: + "../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() + + describe "getPrivilegeLevelForProject", -> + beforeEach -> + @Project.findOne = sinon.stub() + @AuthorizationManager.isUserSiteAdmin = 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 -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) + @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 -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) + @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 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 + + 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 + + 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 -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) + @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 -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) + @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 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 + + 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 + + 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 -> + @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..bc62e603de --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -0,0 +1,237 @@ +sinon = require('sinon') +chai = require('chai') +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 = { + "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 "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 -> + @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/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 6930707249..32de9ebe0a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -24,35 +24,6 @@ describe "CollaboratorsController", -> @project_id = "project-id-123" @callback = sinon.stub() - 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) - @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) - describe "addUserToProject", -> beforeEach -> @req.params = @@ -179,85 +150,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 1fd3a0c2d5..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,12 +17,145 @@ describe "CollaboratorsHandler", -> "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} + "../Errors/Errors": Errors @project_id = "mock-project-id" @user_id = "mock-user-id" @adding_user_id = "adding-user-id" @email = "joe@sharelatex.com" @callback = sinon.stub() + + describe "getMemberIdsWithPrivilegeLevels", -> + 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 + + 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 -> + @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() + @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 "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() + + 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 "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.getProjectsUserIsCollaboratorOf @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 -> 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) -> 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)-> diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index ab003208c2..a98c34af28 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -10,12 +10,13 @@ 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() } "./EditorController": @EditorController = {} '../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} @project_id = "mock-project-id" @doc_id = "mock-doc-id" @@ -76,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 -> @@ -85,19 +97,20 @@ 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", -> 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", -> @@ -105,8 +118,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 @@ -117,7 +130,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", -> @@ -126,7 +139,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 0867fb821f..faa1456b51 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -35,13 +35,10 @@ describe "ProjectController", -> getAllTags: sinon.stub() @NotificationsHandler = getUserNotifications: sinon.stub() - @ProjectModel = - findAllUsersProjects: sinon.stub() - findPopulatedById: sinon.stub() @UserModel = findById: sinon.stub() - @SecurityManager = - userCanAccessProject:sinon.stub() + @AuthorizationManager = + getPrivilegeLevelForProject:sinon.stub() @EditorController = renameProject:sinon.stub() @InactiveProjectManager = @@ -50,6 +47,9 @@ describe "ProjectController", -> markAsOpened: sinon.stub() @ReferencesSearchHandler = indexProjectReferences: sinon.stub() + @ProjectGetter = + findAllUsersProjects: sinon.stub() + getProject: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -67,12 +67,12 @@ describe "ProjectController", -> "../Subscription/LimitationsManager": @LimitationsManager "../Tags/TagsHandler":@TagsHandler "../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 + "./ProjectGetter": @ProjectGetter @user = _id:"!£123213kjljkl" @@ -128,18 +128,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 = @@ -151,6 +139,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)-> @@ -224,7 +225,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)=> @@ -297,10 +298,10 @@ describe "ProjectController", -> fontSize:"massive" theme:"sexy" email: "bob@bob.com" - @ProjectModel.findPopulatedById.callsArgWith 1, null, @project + @ProjectGetter.getProject.callsArgWith 2, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -312,12 +313,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 @@ -339,7 +334,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, null @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee index bdf9f79ece..601f7867b0 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee @@ -33,6 +33,7 @@ describe 'ProjectDeleter', -> '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler "../Tags/TagsHandler":@TagsHandler "../FileStore/FileStoreHandler": @FileStoreHandler = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} "./ProjectGetter": @ProjectGetter 'logger-sharelatex': log:-> @@ -92,6 +93,8 @@ describe 'ProjectDeleter', -> describe "archiveProject", -> beforeEach -> + @CollaboratorsHandler.getMemberIds = sinon.stub() + @CollaboratorsHandler.getMemberIds.withArgs(@project_id).yields(null, ["member-id-1", "member-id-2"]) @ProjectGetter.getProject.callsArgWith(2, null, @project) @Project.update.callsArgWith(2) @@ -111,12 +114,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", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 52c871669a..eede50bfd3 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: "owner" + },{ + 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 98c7310d84..5ab82d611c 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 = {} "logger-sharelatex": err:-> log:-> @@ -134,56 +136,16 @@ describe "ProjectGetter", -> @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) - it "should call find with the project id when string id is passed", (done)-> - @ProjectGetter.getProject @project_id, (err, project)=> - @db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true - assert.deepEqual @project, project - done() - - it "should call find with the project id when object id is passed", (done)-> - @ProjectGetter.getProject ObjectId(@project_id), (err, project)=> - @db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true - assert.deepEqual @project, project - done() - - it "should call the db when a mongoose objectid is used", (done)-> - mongooseID = require('mongoose').Types.ObjectId(@project_id) - @ProjectGetter.getProject mongooseID, (err, project)=> - @db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true - assert.deepEqual @project, project - done() - - describe "populateProjectWithUsers", -> + describe "findAllUsersProjects", -> 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 - + @fields = {"mock": "fields"} + @Project.find = sinon.stub() + @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-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", -> + @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 f43749a5ca..1feef45571 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 'ProjectLocatorTests', -> +describe 'ProjectLocator', -> beforeEach -> Project.getProject = (project_id, fields, callback)=> @@ -301,7 +301,7 @@ describe 'ProjectLocatorTests', -> 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() @@ -310,7 +310,7 @@ describe 'ProjectLocatorTests', -> 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() @@ -319,7 +319,7 @@ describe 'ProjectLocatorTests', -> 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() diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 86cf9111dc..53e064d821 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -39,10 +39,11 @@ describe 'ReferencesHandler', -> get: sinon.stub() post: sinon.stub() } - '../../models/Project': { - Project: @Project = { - findPopulatedById: sinon.stub().callsArgWith(1, null, @fakeProject) - } + '../Project/ProjectGetter': @ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, @fakeProject) + } + '../User/UserGetter': @UserGetter = { + getUser: sinon.stub() } '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null) @@ -70,10 +71,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.findPopulatedById', (done) -> + it 'should call ProjectGetter.getProject', (done) -> @call (err, data) => - @Project.findPopulatedById.callCount.should.equal 1 - @Project.findPopulatedById.calledWith(@projectId).should.equal true + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -109,10 +110,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -129,7 +130,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -147,7 +148,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -167,7 +168,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.findPopulatedById.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')) @@ -182,7 +183,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -234,10 +235,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -254,7 +255,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -272,7 +273,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -312,16 +313,19 @@ describe 'ReferencesHandler', -> beforeEach -> @fakeProject = - owner_ref: - features: - references: false + owner_ref: @owner_ref = "owner-ref-123" + @owner = + features: + references: false + @UserGetter.getUser = sinon.stub() + @UserGetter.getUser.withArgs(@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 +335,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) => 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/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) 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 diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index bb434c8fb6..6e95e4e100 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 "../../infrastructure/Metrics": inc:-> 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 -> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee new file mode 100644 index 0000000000..c6678656b9 --- /dev/null +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -0,0 +1,306 @@ +expect = require("chai").expect +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 [ + (cb) -> + user.request.get "/project/#{project_id}", (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + (cb) -> + 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 = (user, project_id, test, callback) -> + async.series [ + (cb) -> + user.request.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 = (user, project_id, test, callback) -> + async.series [ + (cb) -> + user.request.post { + uri: "/project/#{project_id}/rename" + json: + newProjectName: "new-name" + }, (error, response, body) -> + 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) -> + # 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} + 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_admin_access = (user, project_id, callback) -> + try_admin_access(user, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +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_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.match new RegExp(options.redirect_to) + , callback) + +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.match new RegExp(options.redirect_to) + , callback) + +describe "Authorization", -> + before (done) -> + @timeout(10000) + @owner = new User() + @other1 = new User() + @other2 = new User() + @anon = new User() + @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", -> + before (done) -> + @owner.createProject "private-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + done() + + 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 + + it "should allow the owner admin access to it", (done) -> + expect_admin_access @owner, @project_id, 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 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 + + it "should not allow another user admin access to it", (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 @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) -> + @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, @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, @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, @project_id, redirect_to: "/restricted", done + + it "should allow the read-write user read access to it", (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, @project_id, done + + it "should not allow the read-write user admin access to it", (done) -> + expect_no_admin_access @rw_user, @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, @project_id, done + + 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 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 allow an anonymous user write access to its content", (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 + + 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 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..c13a45499d --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -0,0 +1,77 @@ +request = require("./request") +settings = require("settings-sharelatex") +{db, ObjectId} = 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() + + 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", + 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