diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e7db5d9f65..24bc5743f3 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -99,6 +99,7 @@ module.exports = AuthenticationController = _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" + console.log req.session req.query.redir = req.path url = "/login?#{querystring.stringify(req.query)}" res.redirect url @@ -141,4 +142,5 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser + console.log "LOGGED IN", req.session callback() diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index a6a80edae0..81d62c8b8e 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -1,12 +1,61 @@ -module.exports = - getPrivilegeLevelForProject: (user_id, project_id, callback = (error, canAccess, privilegeLevel) ->) -> - return callback(null, true, "readAndWrite") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +Project = require("../../models/Project").Project +User = require("../../models/User").User + +module.exports = AuthorizationManager = + # Get the privilege level that the user has for the project + # Returns: + # * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has + # access. false if the user does not have access + # * becausePublic: true if the access level is only because the project is public. + getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) -> + getPublicAccessLevel = () -> + Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> + return callback(error) if error? + if project.publicAccesLevel in ["readOnly", "readAndWrite"] + return callback null, project.publicAccesLevel, true + else + return callback null, false, false + if !user_id? + getPublicAccessLevel() + else + CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + if privilegeLevel? and privilegeLevel + # The user has direct access + callback null, privilegeLevel, false + else + getPublicAccessLevel() + canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite", "readOnly"]) + + canUserWriteProjectContent: (user_id, project_id, callback = (error, canWriteContent) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite"]) canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel, becausePublic) -> + return callback(error) if error? + if privilegeLevel == "owner" + return callback null, true + else if privilegeLevel == "readAndWrite" and !becausePublic + return callback null, true + else + return callback null, false canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel == "owner") isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> - \ No newline at end of file + if !user_id? + return callback null, false + User.findOne { _id: user_id }, { isAdmin: 1 }, (error, user) -> + return callback(error) if error? + return callback null, (user?.isAdmin == true) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index a3bce5a2cc..2db1632ecf 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -1,21 +1,101 @@ -module.exports = +AuthorizationManager = require("./AuthorizationManager") +async = require "async" +logger = require "logger-sharelatex" + +module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> - next() + project_ids = (req.query.project_ids or "").split(",") + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + # Remove the projects we have access to. Note rejectSeries doesn't use + # errors in callbacks + async.rejectSeries project_ids, (project_id, cb) -> + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + cb(canRead) + , (unauthorized_project_ids) -> + if unauthorized_project_ids.length > 0 + AuthorizationMiddlewear.redirectToRestricted req, res, next + else + next() ensureUserCanReadProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + if canRead + logger.log {user_id, project_id}, "allowing user read access to project" + next() + else + logger.log {user_id, project_id}, "denying user read access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectSettings: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectSettings user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project settings" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectContent: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectContent user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project content" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanAdminProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserAdminProject user_id, project_id, (error, canAdmin) -> + return next(error) if error? + if canAdmin + logger.log {user_id, project_id}, "allowing user admin access to project" + next() + else + logger.log {user_id, project_id}, "denying user admin access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserIsSiteAdmin: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> + return next(error) if error? + if isAdmin + logger.log {user_id}, "allowing user admin access to site" + next() + else + logger.log {user_id}, "denying user admin access to site" + AuthorizationMiddlewear.redirectToRestricted req, res, next + + _getUserAndProjectId: (req, callback = (error, user_id, project_id) ->) -> + project_id = req.params?.project_id or req.params?.Project_id + if !project_id? + return callback(new Error("Expected project_id in request parameters")) + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return callback(error) if error? + callback(null, user_id, project_id) + + _getUserId: (req, callback = (error, user_id) ->) -> + if req.session?.user?._id? + user_id = req.session.user._id + else + user_id = null + callback null, user_id + + redirectToRestricted: (req, res, next) -> + res.redirect "/restricted" restricted : (req, res, next)-> if req.session.user? diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index f6b8ca7bbd..4f49505af7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -13,11 +13,11 @@ module.exports = CollaboratorsHandler = return callback(error) if error? return callback null, null if !project? members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: "admin" } + members.push { id: project.owner_ref.toString(), privilegeLevel: "owner" } for member_id in project.readOnly_refs or [] - members.push { id: member_id, privilegeLevel: "readOnly" } + members.push { id: member_id.toString(), privilegeLevel: "readOnly" } for member_id in project.collaberator_refs or [] - members.push { id: member_id, privilegeLevel: "readAndWrite" } + members.push { id: member_id.toString(), privilegeLevel: "readAndWrite" } return callback null, members getMemberIds: (project_id, callback = (error, member_ids) ->) -> @@ -33,7 +33,17 @@ module.exports = CollaboratorsHandler = UserGetter.getUser member.id, (error, user) -> return cb(error) if error? return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) - callback + callback + + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> + # In future if the schema changes and getting all member ids is more expensive (multiple documents) + # then optimise this. + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + for member in members or [] + if member.id == user_id?.toString() + return callback null, member.privilegeLevel + return callback null, false getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 776a1c1fb0..ab252472bd 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -34,9 +34,9 @@ module.exports = EditorHttpController = return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !canAccess + if !privilegeLevel callback null, null, false else callback(null, diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ba5ff559f0..ae90f291b2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -225,9 +225,9 @@ module.exports = ProjectController = daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel)-> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> return next(error) if error? - if !canAccess + if !privilegeLevel return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 5b3143d765..fb4e072672 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -27,11 +27,12 @@ module.exports = ProjectEditorHandler = owner = null for member in members - if member.privilegeLevel == "admin" + if member.privilegeLevel == "owner" owner = member.user else result.members.push @buildUserModelView member.user, member.privilegeLevel - result.owner = @buildUserModelView owner, "owner" + if owner? + result.owner = @buildUserModelView owner, "owner" if owner?.features? if owner.features.collaborators? diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee new file mode 100644 index 0000000000..b1a464829c --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -0,0 +1,340 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationManager", -> + beforeEach -> + @AuthorizationManager = SandboxedModule.require modulePath, requires: + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../../models/Project": Project: @Project = {} + "../../models/User": User: @User = {} + @user_id = "user-id-1" + @project_id = "project-id-1" + @callback = sinon.stub() + + describe "getPrivilegeLevelForProject", -> + beforeEach -> + @Project.findOne = sinon.stub() + @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() + + describe "with a private project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "private" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with a public project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "readAndWrite" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "canUserReadProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal false + done() + + describe "canUserWriteProjectContent", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserWriteProjectSettings", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as a collaborator", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as the public", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", true) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserAdminProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "isUserSiteAdmin", -> + beforeEach -> + @User.findOne = sinon.stub() + + describe "when user is admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: true }) + + it "should return true", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal true + done() + + describe "when user is not admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: false }) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when user is not found", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, null) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when no user is passed", -> + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin null, (error, isAdmin) => + @User.findOne.called.should.equal false + expect(isAdmin).to.equal false + done() diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee new file mode 100644 index 0000000000..7e1ca2d5ef --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -0,0 +1,221 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddlewear.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationMiddlewear", -> + beforeEach -> + @AuthorizationMiddlewear = SandboxedModule.require modulePath, requires: + "./AuthorizationManager": @AuthorizationManager = {} + "logger-sharelatex": {log: () ->} + @user_id = "user-id-123" + @project_id = "project-id-123" + @req = {} + @res = {} + @next = sinon.stub() + + METHODS_TO_TEST = { + "ensureUserCanReadProject": "canUserReadProject" + "ensureUserCanWriteProjectSettings": "canUserWriteProjectSettings" + "ensureUserCanWriteProjectContent": "canUserWriteProjectContent" + "ensureUserCanAdminProject": "canUserAdminProject" + } + for middlewearMethod, managerMethod of METHODS_TO_TEST + do (middlewearMethod, managerMethod) -> + describe middlewearMethod, -> + beforeEach -> + @req.params = + project_id: @project_id + @AuthorizationManager[managerMethod] = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with missing project_id", -> + beforeEach -> + @req.params = {} + + it "should return an error to next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.calledWith(new Error()).should.equal true + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserIsSiteAdmin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserCanReadMultipleProjects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + @req.query = + project_ids: "project1,project2" + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 391b533cdf..f659cfebe6 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -36,7 +36,7 @@ describe "CollaboratorsHandler", -> it "should return an array of member ids with their privilege levels", -> @callback .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "admin" } + { id: "owner-ref", privilegeLevel: "owner" } { id: "read-only-ref-1", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } @@ -83,6 +83,26 @@ describe "CollaboratorsHandler", -> ]) .should.equal true + describe "getMemberIdPrivilegeLevel", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, [ + {id: "member-id-1", privilegeLevel: "readAndWrite"} + {id: "member-id-2", privilegeLevel: "readOnly"} + ]) + + it "should return the privilege level if it exists", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-2", @project_id, (error, level) -> + expect(level).to.equal "readOnly" + done() + + it "should return false if the member has no privilege level", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> + expect(level).to.equal false + done() + describe "isUserMemberOfProject", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 1160c81c37..6a594f31ee 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -99,7 +99,7 @@ describe "EditorHttpController", -> describe "when authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, true, "owner") + sinon.stub().callsArgWith(2, null, "owner") @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should find the project without doc lines", -> @@ -128,7 +128,7 @@ describe "EditorHttpController", -> describe "when not authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, false, null) + sinon.stub().callsArgWith(2, null, null) @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should return false in the callback", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 1dbdc8539e..54ea831e05 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -299,7 +299,7 @@ describe "ProjectController", -> @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -332,7 +332,7 @@ describe "ProjectController", -> @ProjectController.loadEditor @req, @res it "should not render the page if the project can not be accessed", (done)-> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, false + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, null @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 7d71f6c89f..eede50bfd3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -49,7 +49,7 @@ describe "ProjectEditorHandler", -> last_name : "ShareLaTeX" email : "owner@sharelatex.com" }, - privilegeLevel: "admin" + privilegeLevel: "owner" },{ user: { _id: "read-only-id" diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index a9533e9533..bed1a56f29 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -44,6 +44,8 @@ class User projectName: name }, (error, response, body) -> return callback(error) if error? + if !body?.project_id? + console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body callback(null, body.project_id) addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> @@ -240,7 +242,7 @@ describe "Authorization", -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @anon, @project_id, redirect_to: "/login", done + expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done